require "edx"
require "toolset"
require "toolset_manager"
require "lsp"

local __last_toolset = nil;

function empty_project_configs()
	return {
		[".breakpoints"] = {};
		[".editor"] = {opened={},states={}};
		[".targets"] = {};
		[".build"] = {};
	};
end;

function show_build_result_notification(success)
	if success then
		show_notify(LANG("notify/build/success"));
	else
		show_notify(LANG("notify/build/failed"));
	end;
end

function show_install_result_notification(success)
	if success then
		show_notify(LANG("notify/build/installed"));
	else
		show_notify(LANG("notify/build/install_failed"));
	end;
end

function reload_cmake_project_settings()
	if CMAKE_PROJECT_PATH then
		-- 更新项目配置脚本
		local config_script = make_path(CMAKE_PROJECT_PATH,"settings.lua");
		if utils:file_time(config_script) then
			dbg_call(function()
					local breakpoints = project_configs and project_configs[".breakpoints"];
					dofile(config_script);
					if breakpoints then
						-- keeps current breakpoints
						project_configs[".breakpoints"] = breakpoints;
					end;
			end, "加载启动脚本出错:");
		end;
	end;
end;

function save_cmake_project_settings(overwrite)
	if CMAKE_PROJECT_PATH == nil or project_configs == nil then
		return;
	end;
	local setting_file_name = make_path(CMAKE_PROJECT_PATH, "settings.lua");
	local setting_file_lines = {};
	local append_line = function(text)
		table.insert(setting_file_lines, text);
	end;

	local original_editor_info = project_configs[".editor"];
	if not overwrite then
		reload_cmake_project_settings();
	end;
	project_configs[".editor"] = original_editor_info;

	local function write_item(indent, item, name)
		indent = indent.."\t";
		local item_value = item[name];
		if type(item_value) == "string" then
			append_line(("%s[\"%s\"] = [[%s]];"):format(indent, name, item_value));
		elseif type(item_value) == "table" then
			append_line(("%s[\"%s\"] = {"):format(indent, name));
			for key, val in pairs(item_value) do
				write_item(indent, item_value, key);
			end;
			append_line(("%s};"):format(indent));
		end;
	end;

	local formatter = {
		[".targets"] = function(target_info)
			for name, info in pairs(target_info) do
				write_item("\t", target_info, name);
			end;
		end;
		[".build"] = function(build_info)
			local target = (menu_bar.get_cmake_default_target() or {}).name;
			local toolset = (menu_bar.get_cmake_toolset() or "");
			local config = (menu_bar.get_cmake_config() or "");
			append_line(("\t\tconfig = [[%s]];"):format(config));
			append_line(("\t\ttarget = [[%s]];"):format(target));
			append_line(("\t\ttoolset = [[%s]];"):format(toolset));
		end;
		[".editor"] = function(editor_info)
			append_line("\t\topened = {");
			for i, filename in ipairs(editor_info.opened) do
				append_line(("\t\t\t[[%s]];"):format(filename));
			end;
			append_line("\t\t};");
			append_line("\t\tstates = {");
			for file, states in pairs(editor_info.states) do
				local file_path = make_path(CMAKE_PROJECT_PATH, file);
				local file_time = utils:file_time(file_path);
				-- trimming status of the file that not exist
				if file_time then
					append_line(("\t\t\t[\"%s\"] = {"):format(file:gsub("\"", "\\\"")));
					for key, value in pairs(states) do
						if key == "cursor" then
							append_line(("\t\t\t\tcursor = {%d;%d};"):format(value[1], value[2]));
						elseif key == "fold" then
							append_line(("\t\t\t\tfold = [[%s]];"):format(value));
						end;
					end;
					append_line("\t\t\t};");
				end;
			end;
			append_line("\t\t};");
		end;
		[".breakpoints"] = function(breakpoints)
			for filename, fileinfo in pairs(breakpoints) do
				append_line(("\t\t[\"%s\"] = {"):format(filename));
				for ln, bkp_info in pairs(fileinfo) do
					append_line(("\t\t\t[%d] = {};"):format(ln));
				end;
				append_line("\t\t};");
			end;
		end;
		[".cmake"] = function(cmake_info)
			for name, info in pairs(cmake_info) do
				write_item("\t", cmake_info, name);
			end;
		end;
	};
	
	append_line("project_configs = {");
	local save_seq = {".targets", ".build", ".editor", ".breakpoints", ".cmake"};
	for idx, name in ipairs(save_seq) do
		append_line(("\t[\"%s\"] = {"):format(name));
		if formatter[name] and project_configs[name] then
			formatter[name](project_configs[name]);
		end;
		append_line("\t};");
	end;
	append_line("};");

	local of=io.open(setting_file_name, "w+");
	of:write(table.concat(setting_file_lines, "\n"));
	of:close();
end;

function cmake_update_include_path_for_opened_documents()
	local all_doc = {mgr.get_all_documents()};
	local def_target = menu_bar.get_cmake_default_target();
	if def_target ~= nil and def_target.include ~= nil then
		local target_include_path = table.concat(def_target.include, ";");
		for idx, doc in ipairs(all_doc) do
			doc.include_path = target_include_path;
		end;
	end;
end;

function find_build_command_cmake(build_target)
	local toolset = menu_bar.get_cmake_toolset(true);
	local config = menu_bar.get_cmake_config();
	return find_build_command_cmake_impl(build_target, toolset, config);
end;

function find_build_command_cmake_impl(build_target, toolset, config, cmake_extra_arguments)
	if toolset == nil then
		return nil;
	end;
	local cmake_cxx = toolset.tools.cxx and string.gsub(toolset.tools.cxx, "\\", "/");
	local cmake_c = toolset.tools.cc and string.gsub(toolset.tools.cc, "\\", "/");
	local cmake_rc = toolset.tools.rc and string.gsub(toolset.tools.rc, "\\", "/");
	if cmake_rc ~= nil then
		cmake_rc = string.gsub(cmake_rc, ";", [[" -DCMAKE_RC_FLAGS="]]);
	end;
	local toolset_name = string.gsub(toolset.name, "[:/\\,*?<>'\"]+", "_");
	local cmake_build_path = make_path(CMAKE_PROJECT_PATH, ".edx/cmake/" .. toolset_name .. "/" .. config);
	local cmake_exe = [[cmake]];
	if not cmake_extra_arguments then
		cmake_extra_arguments = ""
	end;
	local ccache_exe;
	if not toolset.remote then
		ccache_exe = CCACHE_PATH;
	end;
	
	local target_name=toolset["target"] or "";
	if not toolset.remote and (
			target_name == "avr" or
			string.match(target_name, "^arm-.*$") or
			string.match(target_name, "^aarch64-.*$") or
			string.match(target_name, "^mips.*-.*$") or
			string.match(target_name, "^riscv.*-.*$") or
			string.match(target_name, "^xtensa-.*$")
		)
	then
		cmake_extra_arguments = cmake_extra_arguments.." -DCMAKE_SYSTEM_NAME=\""..toolset["target"].."\""
	end;

	if toolset.tools and toolset.tools.ccache then
		ccache_exe = toolset.tools.ccache;
	end;
	if ccache_exe and #ccache_exe > 0 then
		ccache_exe = string.gsub(ccache_exe, "\\", "/");
		cmake_extra_arguments = cmake_extra_arguments..
			[[ -DCMAKE_CXX_COMPILER_LAUNCHER="]]..ccache_exe..
			[[" -DCMAKE_CXX_COMPILER_LAUNCHER="]]..ccache_exe..
			[["]];
	end;
	
	if toolset.tools and toolset.tools.cmake then
		cmake_exe = toolset.tools.cmake;
	elseif not toolset.remote and CMAKE_PATHS and CMAKE_PATHS[1] then
		cmake_exe = CMAKE_PATHS[1];
	end;

	if project_configs and project_configs[".cmake"] then
		local cmake_config_info = project_configs[".cmake"];
		if cmake_config_info.path then
			if utils:file_time(cmake_config_info.path) == nil then
				-- print("user specified cmake [".. cmake_config_info.path.."] is invalid!");
			else
				cmake_exe = cmake_config_info.path;
			end;
		end;
		if type(cmake_config_info.argument) == "string" then
			cmake_extra_arguments = cmake_extra_arguments.." " .. project_configs[".cmake"].argument;
		elseif type(cmake_config_info.argument) == "table" then
			cmake_extra_arguments = cmake_extra_arguments.." " .. table.concat(project_configs[".cmake"].argument, " ");
		end;
	end;

	local cmake_project_path = CMAKE_PROJECT_PATH:gsub("\\", "/");
	cmake_build_path = cmake_build_path:gsub("\\", "/");
	local wrap_command = toolset.wrap_command or function(cmd) return cmd; end;
	local map_path = toolset.map_path or function(path) return path; end;
	local cmake_config_cmd = cmake_exe .. [[ -B "]] ..
		map_path(cmake_build_path) ..
		[[" -S "]] .. map_path(cmake_project_path) ..
		[[" -G "CodeBlocks - Ninja"]] ..
		[[ -DCMAKE_EXPORT_COMPILE_COMMANDS=1]]..
		[[ -DCMAKE_BUILD_TYPE="]] .. config..[["]];
		if cmake_cxx then
			cmake_config_cmd = cmake_config_cmd .. [[ -DCMAKE_CXX_COMPILER="]] .. cmake_cxx .. [["]];
		end;
		if cmake_c then
			cmake_config_cmd = cmake_config_cmd .. [[ -DCMAKE_C_COMPILER="]] .. cmake_c .. [["]];
		end;
		if not toolset.remote and cmake_rc and utils:file_time(cmake_rc) then
			cmake_config_cmd = cmake_config_cmd .. [[ -DCMAKE_RC_COMPILER="]]..cmake_rc..[["]];
		end;
		cmake_config_cmd = cmake_config_cmd..cmake_extra_arguments;
		-- [[" -DCMAKE_MAKE_PROGRAM="]]..cmake_make..

	local def_target = menu_bar.get_cmake_default_target() or { name = ""; output = "" };
	local ninja_exe = toolset.tools["ninja"] or NINJA_PATH;
	local ninja_build_cmd = ninja_exe .. [[ -C "]] .. map_path(cmake_build_path) .. [["]];
	local ninja_clean_cmd = ninja_exe .. [[ -C "]] .. map_path(cmake_build_path) .. [[" -t clean]];
	-- [[SET &&]]..
	local target_cwd = nil;
	local target_argument = nil;
	local target_environment = nil;
	if project_configs ~= nil then
		local user_config = (project_configs[".targets"] and project_configs[".targets"][def_target.name]) or {};
		local config_type = toolset_name.."-"..config;
		local get_config_setting = function(settings)
			if type(settings) == "table" then
				return settings[config_type] or settings["default"];
			elseif type(settings) == "string" then
				return settings;
			end;
			return nil;
		end;
		local cwd = get_config_setting(user_config.cwd);
		local map_path = toolset.map_path or function(path) return path; end;
		if cwd then
			if toolset and toolset.wsl then
				target_cwd = cwd;
			elseif cwd:match([[^%w:\.*$]]) then
				target_cwd = cwd;
			else
				target_cwd = make_path(CMAKE_PROJECT_PATH, cwd);
			end;
		else
			-- print("Can't find target_cwd for config ", config_type);
		end;
		target_argument = get_config_setting(user_config.argument);
		target_environment = get_config_setting(user_config.environment);
	end;
	local debug_type = toolset.tools["debugger"] or "windbg";
	if not target_cwd then
		target_cwd = CMAKE_PROJECT_PATH;
	end;
	return {
		["type"] = "cmake";
		["config"] = wrap_command(cmake_config_cmd);
		["toolset"] = toolset;
		["build"] = wrap_command(ninja_build_cmd);
		["build_target"] = wrap_command(ninja_build_cmd .. " " .. (build_target or def_target.name));
		["clean"] = wrap_command(ninja_clean_cmd);
		["clean_target"] = wrap_command(ninja_clean_cmd .. " " .. (build_target or def_target.name));
		["project_path"] = CMAKE_PROJECT_PATH;
		["build_path"] = cmake_build_path;
		["target_output"] = def_target.output;
		["debug_type"] = debug_type;
		["cwd"] = target_cwd;
		["argument"] = target_argument;
		["environment"] = target_environment;
	};
end;

function find_build_command(build_target, skip_current_file)
	if skip_current_file then
	else
		local explain_vars = function(vars)
			return function(key)
				if vars[key] ~= nil then
					return vars[key];
				end
				return os.getenv(key);
			end;
		end;
		local cur_doc = mgr.current_document;
		if cur_doc.content_type == content_type_cxx or
			cur_doc.content_type == content_type_dlang
		then
			local current_file = cur_doc.file_name;
			local current_path = make_path(current_file, "..").."\\";
			local current_file_suffix = current_file:gsub("^.*%.(%w+)$", "%1");
			local is_cpp = current_file_suffix and current_file_suffix:lower() ~= "c";
			local make_compiler_path = function(info)
				local repl_count = 0;
				local cmd, repl = string.gsub(info, ";", function(v)
						repl_count = repl_count + 1;
						if repl_count == 1 then
							return "\" ";
						end;
						return " ";
				end);

				if repl == 0 then
					return [["]] .. info ..[["]];
				end;
				return [["]] .. cmd;
			end;
			local get_compiler = function(tools)
				if is_cpp then
					return make_compiler_path(tools.cxx);
				end;
				return make_compiler_path(tools.cc);
			end;
			local result = { ["type"] = "direct" };
			local build_toolset;
			local build_command;
			local dbg_type, dbg_target;
			local build_lib;
			local build_lib_path;
			local build_include;
			local build_flags;
			local linker_flags = "";
			local dbg_argument = nil;
			local dbg_cwd = nil;
			local dbg_env = nil;

			local map_path = function(path) return path; end;
			local wrap_command = function(cmd) return cmd; end;
			local suffixes = {
				exec = ".exe";
				shared = ".dll";
				lib = ".lib";
			};

			for i = 0, 50, 1 do
				local line = mgr.current_document:get_text(i, 0, 512);
				if line ~= nil then
					local opt_type, opt_value = line:match([[^//%s*([_%w][_%w%d]-)%s*:%s*(.*)]]);
					if opt_type == nil then
						break;
					end;

					if not build_toolset and opt_type == "toolset" then
						build_toolset = opt_value;
					end;

					if not build_command and opt_type == "build" then
						build_command = opt_value;
					end;

					if not build_lib and opt_type == "lib" then
						build_lib = opt_value;
					end;

					if not build_lib_path and opt_type == "lib_path" then
						build_lib_path = opt_value;
					end;

					if not build_include and opt_type == "include" then
						build_include = opt_value;
					end;

					if not build_flags and opt_type == "flags" then
						build_flags = opt_value;
					end;

					if not linker_flags and opt_type == "ld_flags" then
						linker_flags = opt_value;
					end;

					if not dbg_type and opt_type == "debug" then
						dbg_type, dbg_target = opt_value:match([[([^:]-):(.*)]]);
					end;
					
					if not dbg_argument and opt_type == "args"  then
						dbg_argument = opt_value;
					end;

					if not dbg_cwd and opt_type == "cwd" then
						dbg_cwd = opt_value;
					end;

					if opt_type == "env" then
						local env_key, env_val = opt_value:match([[^([_%w][_%w%d]-)%s*=%s*(.*)$]]);
						if env_key then
							if not dbg_env then
								dbg_env = {};
							end;
							dbg_env[env_key] = env_val;
						end;
					end;
				end;
			end;

			if TOOLSETS and build_toolset then
				for i, ts in ipairs(TOOLSETS) do
					if ts.name == build_toolset then	
						result.toolset = ts;
						break;
					end;
				end;
				if not result.toolset then
					result.toolset = TOOLSETS[tonumber(build_toolset)];
				end;
				if not result.toolset then
					result.toolset = TOOLSETS[1];
				end;
				if result.toolset then
					map_path = result.toolset.map_path or map_path;
					wrap_command = result.toolset.wrap_command or wrap_command;
					suffixes = result.toolset.suffixes or suffixes;
				end;
			end;

			local explainer = explain_vars({
					["THIS_FILE"] = map_path(current_file);
					["THIS_FILE:PATH"] = map_path(current_path);
					["THIS_FILE:BASENAME"] = get_base_name(current_file);
					["THIS_FILE:NAME"] = get_file_name(current_file);
			});

			if build_command == nil and result.toolset then
				if result.toolset.tools.msvc then
					-- use msvc cl command line
					build_command = [[]] .. get_compiler(result.toolset.tools) .. [[ /Zi ${THIS_FILE} /Fe:${THIS_FILE:PATH}${THIS_FILE:BASENAME}]] .. suffixes["exec"]
					dbg_type = result.toolset.tools["debugger"] or [[windbg]]
				else
					-- use gcc command line
					build_command = [[]] .. get_compiler(result.toolset.tools) .. [[ -g ${THIS_FILE} -o ${THIS_FILE:PATH}${THIS_FILE:BASENAME}]] .. suffixes["exec"];
					dbg_type = result.toolset.tools["debugger"] or [[gdb/mi]];
				end;
				if dbg_target == nil or dbg_target == "" then
					dbg_target = [[${THIS_FILE:PATH}${THIS_FILE:BASENAME}]] .. suffixes["exec"];
				end;
			end;

			if dbg_type ~= nil then
				dbg_target = string.gsub(dbg_target, [[%${([-.%$%w%d:_]+)}]], explainer);
				result.debug_type = dbg_type;
				result.target_output = dbg_target;
			end;

			if dbg_cwd ~= nil then
				result.cwd = dbg_cwd;
			end;

			if dbg_argument ~= nil then
				result.argument = dbg_argument;
			end;

			if build_command ~= nil then
				if build_flags then
					if result.toolset then
						build_flags = " "..build_flags;
						if result.toolset.tools.msvc then
							build_flags = build_flags:gsub("%s%-Wa,%-mbig%-obj", " /bigobj");
							build_flags = build_flags:gsub("%s%-fexceptions", " /EHsc");
							build_flags = build_flags:gsub("%s%-fno%-exceptions", " /EHsc-");
							build_flags = build_flags:gsub("%s%-O3", " /Ox");
							build_flags = build_flags:gsub("%s%-O0", " /Od");
							build_flags = build_flags:gsub("%s%-mtune=%S*", "");
							build_flags = build_flags:gsub("%s%-fstack%-check=no", " /GS-");
							build_flags = build_flags:gsub("%s%-fstack%-check=%S+", " /GS");
							build_flags = build_flags:gsub("%s%-fno%-stack%-check", " /GS-");
							build_flags = build_flags:gsub("%s%-fstack%-check", " /GS");
							build_flags = build_flags:gsub("%s%-std=gnu%+%+", " /std:c++");
							build_flags = build_flags:gsub("%s%-std=", " /std:");
							if build_flags:match("%s%-static%-libstdc%+%+") then
								build_flags = build_flags:gsub("%s%-static%-libstdc%+%+", " /MT");
								build_flags = build_flags:gsub("%s%-static%-libgcc", "");
								build_flags = build_flags:gsub("%s%-static", "");
								build_flags = build_flags:gsub("%s%-dn", "");
								build_flags = build_flags:gsub("%s%-non_shared", "");
							elseif build_flags:match("%s%-static%-libgcc") then
								build_flags = build_flags:gsub("%s%-static%-libgcc", " /MT");
								build_flags = build_flags:gsub("%s%-static", "");
								build_flags = build_flags:gsub("%s%-dn", "");
								build_flags = build_flags:gsub("%s%-non_shared", "");
							else
								build_flags = build_flags:gsub("%s%-static", " /MT");
								build_flags = build_flags:gsub("%s%-non_shared", " /MT");
								build_flags = build_flags:gsub("%s%-dn", " /MT");
							end;
							build_flags = build_flags:gsub("%s%-Bdynamic", " /MD");
							build_flags = build_flags:gsub("%s%-dy", " /MD");
							build_flags = build_flags:gsub("%s%-call_shared", " /MD");
							build_flags = build_flags:gsub("%s%-fPIC", "");
							build_flags = build_flags:gsub("%s%-fPIE", "");
							build_flags = build_flags:gsub("%s%-fpic", "");
							build_flags = build_flags:gsub("%s%-fpie", "");
							build_flags = build_flags:gsub("%s%-g", " /Zi");
							build_flags = build_flags:gsub("%s%-ggdb", " /Zi");
							build_flags = build_flags:gsub("%s%-gcoff", " /Zi");
							build_flags = build_flags:gsub("%s%-gdwarf", " /Zi");
							if build_flags:match("%s%-s") or build_flags:match("%s%-S") then
								build_flags = build_flags:gsub("%s%-s", " ");
								build_flags = build_flags:gsub("%s%-S", " ");
								linker_flags = linker_flags .. " /Release";
							end;
							if build_flags:match("%s/[Rr]elease") then
								build_flags = build_flags:gsub("%s/[Rr]elease", " ");
								linker_flags = linker_flags .. " /Release";
							end;
							if build_flags:match("%s/[Dd]ebug") then
								build_flags = build_flags:gsub("%s/[Dd]ebug", " ");
								linker_flags = linker_flags .. " /Debug";
							end;
							if build_flags:match("%s/mwindows") then
								build_flags = build_flags:gsub("%s/mwindows", " ");
								linker_flags = linker_flags .. " /SUBSYSTEM:Windows";
							end;
							build_flags = build_flags:gsub("%s%-flto", " /GL");

							build_flags = build_flags:gsub("%s%-", " /");
							if result.toolset.version == 17 then
								build_flags = build_flags:gsub("%s/std:c%+%+2[3ab]", " /std:c++latest");
								build_flags = build_flags:gsub("%s/std:c%+%+20", " /std:c++20");
							else
								build_flags = build_flags:gsub("%s/std:c%+%+2[03ab]", " /std:c++latest");
							end
							build_flags = build_flags:gsub("%s/std:c%+%+1z", " /std:c++17");
							build_flags = build_flags:gsub("%s/std:c%+%+1[1y]", " /std:c++14");
							build_flags = build_flags:gsub("%s/std:c%+%+0x", " /std:c++14");
							build_flags = build_flags:gsub("%s/std:c%+%+98", " /std:c++14");
						else
							build_flags = build_flags:gsub("%s/EHsc%-", " -fno-exceptions");
							build_flags = build_flags:gsub("%s/GX%-", " -fno-exceptions");
							build_flags = build_flags:gsub("%s/EHsc", " -fexceptions");
							build_flags = build_flags:gsub("%s/GX", " -fexceptions");
							build_flags = build_flags:gsub("%s/Ox", " -O3 -mtune=native");
							build_flags = build_flags:gsub("%s/Od", " -O0");
							build_flags = build_flags:gsub("%s/GS%-", " -fstack-check=no");
							build_flags = build_flags:gsub("%s/GS", " -fstack-check");
							build_flags = build_flags:gsub("%s/Ge", " -fstack-check");
							build_flags = build_flags:gsub("%s/std:", " -std=");
							build_flags = build_flags:gsub("%s/bigobj", " -Wa,-mbig-obj");
							build_flags = build_flags:gsub("%s/MTd", " -static -static-libgcc -static-libstdc++");
							build_flags = build_flags:gsub("%s/MT", " -static -static-libgcc -static-libstdc++");
							build_flags = build_flags:gsub("%s/MD", " ");
							build_flags = build_flags:gsub("%s/MDd", " ");
							build_flags = build_flags:gsub("%s/GL%-", " ");
							build_flags = build_flags:gsub("%s/GL", " -flto");
							build_flags = build_flags:gsub("%s/Zi", " -g");
							build_flags = build_flags:gsub("%s/Debug", " -g");
							build_flags = build_flags:gsub("%s/Release", " -s");

							build_flags = build_flags:gsub("%s%-std=c%+%+latest", " -std=c++20");

							build_flags = build_flags:gsub("%s/", " -");
						end;
						build_flags = build_flags:gsub("^%s*", "");
					end;
					build_command = build_command .. " " .. build_flags;
				end

				if build_include and result.toolset then
					if result.toolset.tools.msvc then
						build_include = build_include:gsub("([-_%d%w%+%./\\]+)([,;$]?)", " /I%1");
					else
						build_include = build_include:gsub("([-_%d%w%+%./\\]+)([,;$]?)", " -I%1");
					end;
					build_command = build_command .. build_include;
				end;

				if build_lib and result.toolset then
					local lib_search_paths = nil;
					if build_lib_path then
						lib_search_paths = {build_lib_path:match("([-_%d%w%+%./\\]+)[,;$]?")};
					end;
					local is_lib_file_exists = function()
						return false;
					end;
					if lib_search_paths then
						is_lib_file_exists = function(name)
							for i, path in ipairs(lib_search_paths) do
								lib_path = make_path(current_path, path, name);
								if utils:file_time(lib_path) then
									return true;
								end;
							end;
							return false;
						end;
					end;
					
					if result.toolset.tools.msvc then
						build_lib = build_lib:gsub("([-_%d%w%+%.]+)([,;$]?)", function(name)
								if is_lib_file_exists(name) then
									return " "..name;
								end;
								return " "..name..".lib";
						end);
					else
						build_lib = build_lib:gsub("([-_%d%w%+%.]+)([,;$]?)", function(name)
								if is_lib_file_exists(name) then
									local sname = name:match("^lib(.*)%.a$");
									if sname then
										return " -l"..sname;
									end;
									return " "..name;
								end;
								return " -l"..name;
						end);
					end;
					build_command = build_command .. build_lib;
				end;

				if build_lib_path and result.toolset then
					if result.toolset.tools.msvc then
						build_lib_path = build_lib_path:gsub("([-_%d%w%+%./\\]+)([,;$]?)", " /LIBPATH:%1");
						build_command = build_command .. " /link" .. build_lib_path;
					else
						build_lib_path = build_lib_path:gsub("([-_%d%w%+%./\\]+)([,;$]?)", " -L%1");
						build_command = build_command .. build_lib_path;
					end;
				end;

				if linker_flags and result.toolset and result.toolset["type"] == "msvc" then
					if linker_flags:match("%s/[Rr]elease") then
						linker_flags = linker_flags:gsub("%s/[Dd]ebug", " ");
						build_command = build_command:gsub("%s/Zi%s", " ");
					end;
					if build_command:match("%s/Zi") then
						linker_flags = linker_flags:gsub("%s/[Dd]ebug", " ");
					end;
					linker_flags = linker_flags:gsub("%s+", " ");
					if linker_flags ~= nil and linker_flags ~= " " and linker_flags ~= "" then
						if build_command:match("%s/link%s") then
							build_command = build_command .. linker_flags;
						else
							build_command = build_command .. " /link" .. linker_flags;
						end;
					end;
				end;

				local cmd = string.gsub(build_command, [[%${([-.%$%w%d:_]+)}]], explainer);

				local dbg_target_time = dbg_target and utils:file_time(dbg_target);
				if dbg_target_time and dbg_target_time >= utils:file_time(current_file) then
					result.build = "cmd /d /c echo nothing to do!";
				else
					result.build = wrap_command(cmd);
				end;
				result.project_path = current_path;
			end;

			if not result.cwd then
				result.cwd = current_path;
			end;

			if dbg_env then
				local env_cache = {};
				for item_key, val in pairs(dbg_env) do
					val = string.gsub(val, [[%${([-.%$%w%d:_]+)}]],
						function(key)
							local env_val = env_cache[key];
							if env_val then
								return env_val;
							end;
							env_val = explainer(key);
							env_cache[key] = env_val;
							return env_val;
						end
					);
					env_cache[item_key] = val;
					dbg_env[item_key] = val;
				end;
				result.environment = dbg_env;
			end;

			if result.build or result.target_output then
				return result;
			end;
		end;
	end;
	if CMAKE_PROJECT_PATH then
		return find_build_command_cmake(build_target);
	end
end;

function build_toolset_params(cmd)
	local toolset = cmd.toolset;
	local param_path = {table.unpack(toolset.bin)};
	table.insert(param_path,os.getenv("PATH"));
	local params = {
		[1] = cmd.project_path;
		["PATH"] = table.concat(param_path,  ";");
		["INCLUDE"] = table.concat(toolset.include, ";");
		["LIB"] = table.concat(toolset.lib, ";");
	};
	if toolset.env then
		for name, val in pairs(toolset.env) do
			params[name] = val;
		end;
	end;
	return params;
end;

function cmake_reload_cbp(cmd) 
	local cbp_file = utils:list_dir(make_path(cmd.build_path, "*.cbp"));
	cbp_file = make_path(cmd.build_path, cbp_file);
	local cbp = utils:load_cbp(cbp_file);
	local toolset = cmd.toolset;
	-- fix mapped include path
	if toolset and toolset.unmap_path then
		local unmap_path = toolset.unmap_path;
		for idx, target in ipairs(cbp) do
			if target and type(target.include) == "table" then
				for n, path in ipairs(target.include) do
					target.include[n] = unmap_path(path);
				end;
			end;
		end;
	end;
	table.sort(cbp,
		function(a,b)
			return a.name < b.name;
		end
	);
	CMAKE_PROJECT_CBP = cbp;
	menu_bar.update_cmake_target(cbp);
end;

function handle_msvc_build_result(edx, output_lines, ctx, unmap_path) 
	local last_type = ctx.last_type;
	local main_file = ctx.main_file or "";
	local type_id = {
		["error"] = 1;
		["warning"] = 2;
		["note"] = 3;
	};
	output_lines = output_lines:gsub("(\r\n)", "\n");
	for output_line in output_lines:gmatch("([^\n]*)") do
		if output_line ~= "" then
			local result = {output_line:match("(.+)%((%d+),?(%d*)%):%s*(%w+)%s*([%w%d]*):%s*(.+)$")};
			local result_type = result[4];
			if result_type == "warning" or result_type == "error" then
				main_file = result[1];
				last_type = result_type;
				ctx[result_type] = ctx[result_type] + 1;
			elseif result_type == "note" then
				last_type = "note";
			elseif output_line == "        with" or (last_type == "note" and output_line:match("(%s%s%s%s+)")) then
				result = {
					"", "", "", "", output_line
				};
				last_type = "note";
			else
				main_file = "";
				last_type = nil;
			end;
			if last_type then
				local file_name = result[1];
				local line = tonumber(result[2]);
				local column = 0;
				local location;
				if result[3] then
					location = file_name.. ":"..result[2]..":"..result[3]..":";
					column = tonumber(result[3]);
				else
					location = file_name.. "("..result[2]..")";
				end;
				local code = result[5];
				local message = result[6];
				if main_file == file_name then
					mgr:add_doc_diagnoistic("compiler", file_name, line, column, line, -1, type_id[last_type], message);
				end;
				edx:add_build_result(type_id[last_type], main_file, file_name, line, location, code, message);
			end;
		end;
	end;
	ctx.last_type = last_type;
	ctx.main_file = main_file;
end;

function handle_gcc_build_result(edx, output_lines, ctx, unmap_path) 
	local last_type = ctx.last_type;
	local main_file = ctx.main_file or "";
	local type_id = {
		["error"] = 1;
		["warning"] = 2;
		["note"] = 3;
	};
	function flush_tips(last_main, new_main, new_type)
		if last_main == "" or last_type == nil then
			return;
		end;
		local include_tip = ctx.include_tip;
		local inline_tip = ctx.inline_tip;
		local type_id_val = type_id[last_type];
		if include_tip then
			for idx, info in ipairs(include_tip) do
				local location_base = info.file..":"..info.line;
				local location = location_base..":1:";
				edx:add_build_result(type_id_val, main_file, info.file, tonumber(info.line), location, "", "include from "..location_base);
			end;
		end;
		if inline_tip then
			if include_tip then
				edx:add_build_result(type_id_val, main_file, "", 0, "", "", "----------------------------------------");
			end;
			for idx, info in ipairs(inline_tip) do
				local location = info.file..":"..info.line..":"..info.col..":";
				edx:add_build_result(type_id_val, main_file, info.file, tonumber(info.line), location, "", info.info);
			end;
		end;
		ctx.include_tip = nil;
		ctx.inline_tip = nil;
		main_file = new_main;
		last_type = new_type;
	end;
	for output_line in output_lines:gmatch("([^\n]*)") do
		local file_tip = {output_line:match("^I?n? ?f?i?l?e? ?i?n?c?l?u?d?e?d?%s+from%s+(.*):(%d+)[:,]$")};
		if #file_tip > 0 then
			flush_tips(main_file, "", nil);
			output_line = "";
			ctx.include_tip = ctx.include_tip or {};
			table.insert(ctx.include_tip, {
					["file"] = file_tip[1];
					["line"] = file_tip[2];
			});
		else
			file_tip = {output_line:match("^%s+(inlined%s+from%s+'.*')%s+at%s+(.*):(%d+):(%d+)[:,]$")};
			if #file_tip > 0 then
				flush_tips(main_file, "", nil);
				output_line = "";
				ctx.inline_tip = ctx.inline_tip or {};
				table.insert(ctx.inline_tip, {
						["info"] = file_tip[1];
						["file"] = file_tip[2];
						["line"] = file_tip[3];
						["col"] = file_tip[4];
				});
			end;
		end;
		
		local result = {output_line:match("(.*):(%d+):(%d+):%s*(%w*%s*[%w%d]*)%s*:%s*(.*)")};
		local result_type = result[4];
		if result_type == "fatal error" then
			result_type = "error";
		end;
		if result_type == "warning" or result_type == "error" then
			flush_tips(main_file, "", nil);
			main_file = unmap_path(result[1]);
			last_type = result_type;
			ctx[result_type] = ctx[result_type] + 1;
		elseif result_type == "note" then
			last_type = "note";
		elseif (last_type == "note" or last_type == "error" or last_type == "warning")
			and (output_line:match("^(%s%s%s%s+)|") or output_line:match("^(%s+%d+%s+)|"))
		then
			last_type = "note";
			result = {
				"", "", "", "", output_line:match("%s+%d*%s*|(.*)")
			};
			local diag = ctx.__diagnoistic;
			if diag then
				local matched_directive = output_line:match("%s+|%s*(%^~*)");
				if matched_directive then
					ctx.__diagnoistic = nil;
					local begin_col = diag["column"];
					local end_col = begin_col + (#matched_directive);
					mgr:add_doc_diagnoistic("compiler", diag["file"], diag["line"], begin_col, diag["line"], end_col, diag["type"], diag["message"]);
				end;
			end;
		elseif output_line == "" or output_line == nil then
			-- skip the empty line
			output_line = nil;
		else
			flush_tips(main_file, "", nil);
			main_file = "";
			last_type = nil;
		end;
		if last_type and output_line then
			local file_name = result[1];
			file_name = unmap_path(file_name);
			local line = tonumber(result[2]);
			local column = tonumber(result[3]);
			local location = file_name.. ":"..result[2]..":"..result[3]..":";
			local code = "";
			local message = result[5];
			if last_type ~= "note" and main_file == file_name then
				ctx.__diagnoistic = {
					["file"] = file_name;
					["line"] = line;
					["column"] = column;
					["message"] = message;
					["type"] = type_id[last_type];
				};
				mgr:add_doc_diagnoistic("compiler", file_name, line, column, line, -1, type_id[last_type], message);
			end;
			edx:add_build_result(type_id[last_type], main_file, file_name, line, location, code, message);
		end;
	end;
	ctx.last_type = last_type;
	ctx.main_file = main_file;
end;

function gen_clang_result_handler(result_line_pattern)
	return function(edx, output_lines, ctx, unmap_path)
		local last_type = ctx.last_type;
		local main_file = ctx.main_file or "";
		local type_id = {
			["error"] = 1;
			["warning"] = 2;
			["note"] = 3;
		};
		for output_line in output_lines:gmatch("([^\n]*)") do
			local result = {output_line:match(result_line_pattern)};
			local result_type = result[4];
			if result_type == "warning" or result_type == "error" then
				main_file = make_path(unmap_path(result[1]));
				last_type = result_type;
				ctx[result_type] = ctx[result_type] + 1;
			elseif result_type == "note" then
				last_type = "note";
			elseif (last_type == "note" or last_type == "error" or last_type == "warning") then
				if output_line:match("(In file included from).*") then
					last_type = "note";
					local ret = {output_line:match("In file included from (.*):(%d+):.*")}
					result = {
						ret[1], ret[2], "", "", "", output_line
					};
				elseif output_line:match("(%s%s%s%s+)") then
					last_type = "note";
					result = {
						"", "", "", "", "", output_line
					};
				else
					main_file = "";
					last_type = nil;
				end;
			else
				main_file = "";
				last_type = nil;
			end;
			if last_type then
				local file_name = result[1];
				file_name = unmap_path(file_name);
				if file_name ~= "" then
					file_name = make_path(file_name);
				end;
				local line = tonumber(result[2]);
				local column = tonumber(result[3]);
				local location = "";
				if file_name ~= "" then
					if result[3] ~= "" then
						location = file_name.. ":"..result[2]..":"..result[3]..":";
					else
						location = file_name.. "("..result[2].."):";
					end;
				end;
				local code = result[5];
				local message = result[6];
				if message == nil or file_name == nil or code == nil then
					print("ERROR > ", output_line);
				else
					if main_file == file_name then
						mgr:add_doc_diagnoistic("compiler", file_name, line, column, line, -1, type_id[last_type], message);
					end;
					edx:add_build_result(type_id[last_type], main_file, file_name, line, location, code, message);
				end;
			end;
		end;
		ctx.last_type = last_type;
		ctx.main_file = main_file;
	end;
end;

local handle_clang_build_result = gen_clang_result_handler("(.*):(%d+):(%d+):%s*(%w*)%s*([%w%d]*):%s*(.*)");
local handle_clang_cl_build_result = gen_clang_result_handler("(.*)%((%d+),(%d+)%):%s*(%w*)%s*([%w%d]*):%s*(.*)");

--[[
cmd -- find_build_command() result
build_type -- the build type string. "build", "build_target", "rebuild", "rebuild_target"
on_complete -- task finish callback
]]
function build_cmake_do_build_params(cmd, build_type, on_complete, prepend_cmds, apppend_cmds)
	local cbp_file = utils:list_dir(make_path(cmd.build_path, "*.cbp"));
	local cmd_lines = {};
	if cbp_file == nil or cmd.config ~= CMAKE_LAST_CONFIG[cmd.build_path] then
		-- not configed or config changed
		CMAKE_LAST_CONFIG[cmd.build_path] = cmd.config;
		cmd_lines = {cmd.config};
	end;
	if build_type then
		if build_type == "rebuild" then
			table.insert(cmd_lines, cmd["clean"]);
			table.insert(cmd_lines, cmd["build"]);
		elseif build_type == "rebuild_target" then
			table.insert(cmd_lines, cmd["clean_target"]);
			table.insert(cmd_lines, cmd["build_target"]);
		else
			table.insert(cmd_lines, cmd[build_type]);
		end;
	else
		cmd_lines = {cmd.config};
	end;

	if prepend_cmds then
		for i, val in ipairs(prepend_cmds) do
			table.insert(cmd_lines, i, val);
		end;
	end;
	if apppend_cmds then
		for i, val in ipairs(apppend_cmds) do
			table.insert(cmd_lines, val);
		end;
	end;

	local toolset = cmd.toolset;
	local begin_clock = os.clock();

	local result_handler_ctx = {["warning"]=0, ["error"]=0};
	local result_handler = handle_msvc_build_result;

	local encoding = toolset.encoding or "";
	local remote = toolset.remote;
	if toolset and toolset.tools then
		if toolset.tools.clang then
			if toolset.tools.msvc then
				result_handler = handle_clang_cl_build_result;
			else
				result_handler = handle_clang_build_result;
			end;
		elseif toolset.tools.gcc then
			result_handler = handle_gcc_build_result;
		elseif toolset.tools.msvc then
			result_handler = handle_msvc_build_result;
		end;
	end;

	local unmap_path = function(path) return path; end;
	local map_path = function(path) return path; end;
	if toolset and toolset.unmap_path then
		unmap_path = toolset.unmap_path;
		map_path = toolset.map_path;
	end;

	local param_path = nil;
	if remote then
		param_path = nil;
	else
		param_path = os.getenv("PATH")..table.concat({table.unpack(toolset.bin)}, ";");
	end;

	local toolset_envs = (toolset.envs or {});
	local do_build_params = {
		desc = "build";
		cwd = cmd.project_path;
		["encoding"] = encoding;
		["envs"] = table.clone(
			toolset_envs,
			{
				["PATH"] = param_path;
				["INCLUDE"] = table.concat(toolset.include, ";");
				["LIB"] = table.concat(toolset.lib, ";");
				["CCACHE_DIR"] = toolset_envs["CCACHE_DIR"] or map_path(make_path(CMAKE_PROJECT_PATH, ".edx/ccache"));
			}
		);
		["cmds"] = cmd_lines;
		on_output = function(cmd_idx, output_lines)
			if remote and (cmd_idx == 1 or cmd_idx == #cmd_lines) then
				print(output_lines);
				return;
			end;
			dbg_call(
				function()
					result_handler(edx, output_lines, result_handler_ctx, unmap_path);
			end, "");
		end;
		on_command_begin = function(cmd_idx)
			if cmd_idx == 1 then
				edx:clear("build_result");
				menu_bar:disable_cmake_menu_items();
			end;
			if type(cmd_lines[cmd_idx]) == "table" then
				print(cmd_idx, " > ", cmd_lines[cmd_idx].cmd);
			else
				print(cmd_idx, " > ", cmd_lines[cmd_idx]);
			end;
		end;
		on_command_end = function(cmd_idx, ret_code)
			print(">>>>");
		end;
		on_finished = function(success)
			menu_bar:enable_cmake_menu_items();
			local doc = mgr.current_document;
			doc:reset_render();

			local success_info = "failed";
			if success then
				cmake_reload_cbp(cmd);
				success_info = "finished";
			end;
			print(("task %s %.3f s, %d error(s), %d warning(s)"):format( success_info, os.clock() - begin_clock, result_handler_ctx["error"], result_handler_ctx["warning"]));
			if on_complete then
				on_complete(success);
			end;
		end;
		["remote"] = remote;
	};

	if remote then
		-- add sync operations
		table.insert(cmd_lines, 1, "@UPLOAD \""..CMAKE_PROJECT_PATH.."\" \""..map_path(CMAKE_PROJECT_PATH).."\"");
		table.insert(cmd_lines, "@DOWNLOAD \""..map_path(CMAKE_PROJECT_PATH).."/.edx/cmake\" \""..CMAKE_PROJECT_PATH.."\\.edx\\cmake\"");
	end;

	return do_build_params;
end;

function build_do_build_params(cmd, build_type, on_complete, prepend_cmds, apppend_cmds)
	local cmd_lines = {};
	if build_type then
		if build_type == "rebuild" then
			table.insert(cmd_lines, cmd["clean"]);
			table.insert(cmd_lines, cmd["build"]);
		elseif build_type == "rebuild_target" then
			table.insert(cmd_lines, cmd["clean_target"]);
			table.insert(cmd_lines, cmd["build_target"]);
		else
			table.insert(cmd_lines, cmd[build_type]);
		end;
	else
		cmd_lines = {cmd.config};
	end;

	if prepend_cmds then
		for i, val in ipairs(prepend_cmds) do
			table.insert(cmd_lines, i, val);
		end;
	end;
	if apppend_cmds then
		for i, val in ipairs(apppend_cmds) do
			table.insert(cmd_lines, val);
		end;
	end;

	local toolset = cmd.toolset or {bin={};include={};lib={};tools={}};
	local tools = toolset.tools;
	local param_path = {table.unpack(toolset.bin)};
	table.insert(param_path,os.getenv("PATH"));

	local begin_clock = os.clock();

	local result_handler_ctx = {["warning"]=0,["error"]=0};
	
	local result_handler = handle_msvc_build_result;

	local encoding = toolset.encoding or "";

	local remote = toolset.remote;
	
	if tools.clang then
		if tools.msvc then
			result_handler = handle_clang_cl_build_result;
		else
			result_handler = handle_clang_build_result;
		end;
	elseif tools.gcc then
		result_handler = handle_gcc_build_result;
	elseif tools.msvc then
		result_handler = handle_msvc_build_result;
	end;

	local unmap_path = function(path) return path; end;
	if toolset and toolset.unmap_path then
		unmap_path = toolset.unmap_path;
	end;

	local do_build_params = {
		desc = "build";
		cwd = cmd.project_path;
		["encoding"] = encoding;
		["envs"] = {
			["PATH"] = table.concat(param_path,  ";");
			["INCLUDE"] = table.concat(toolset.include, ";");
			["LIB"] = table.concat(toolset.lib, ";");
		};
		["cmds"] = cmd_lines;
		on_output = function(cmd_idx, output_lines)
			result_handler(edx, output_lines, result_handler_ctx, unmap_path);
		end;
		on_command_begin = function(cmd_idx)
			if cmd_idx == 1 then
				edx:clear("build_result");
			end;
			print(cmd_idx, " > ", cmd_lines[cmd_idx]);
		end;
		on_command_end = function(cmd_idx, ret_code)
			print(">>>>");
		end;
		on_finished = function(success)
			local doc = mgr.current_document;
			doc:reset_render();
			local success_info = "failed";
			if success then
				success_info = "finished";
			end;
			print(("task %s %.3f s, %d error(s), %d warning(s)"):format( success_info, os.clock() - begin_clock, result_handler_ctx["error"], result_handler_ctx["warning"]));
			if on_complete then
				on_complete(success);
			end;
		end;
		["remote"] = remote;
	};

	return do_build_params;
end;

function open_terminal(open_path,skip_current_file)
	local cmd = find_build_command(nil,skip_current_file);
	local full_wt, wt_root, wt = find_windows_terminal();
	local cmd_line = "start cmd.exe /K pushd ".."\""..open_path.."\"";
	local working_dir = os.getenv("USERPROFILE") or os.getenv("SystemRoot") or [[c:\]];
	if wt ~= nil then
		-- !!! merge new terminal into exists WindowsTerminal frame as a new tabpage, environment will lost
		-- cmd_line = [["]]..wt..[[ -w 0 cmd.exe /K pushd """]]..open_path..[[""""]];
		cmd_line = [["]]..wt..[[ cmd.exe /K pushd """]]..open_path..[[""""]];
		working_dir = wt_root;
	end;
	if cmd ~= nil and cmd.type == "cmake" then
		local params = build_toolset_params(cmd);
		params[1] = working_dir;
		edx.shell2(cmd_line, "", params);
	else
		edx.shell2(cmd_line, "", working_dir);
	end
end;

-- 打开工程中的文件
function xws_mgr.on_open_file(self, id, file_path, mode)
	if mgr:open_doc({mode, file_path}) then
		edx:active("editor");
	end;
	return true;
end;

function is_file_in_path(file_path, folder)
	if #file_path >= #folder then
		return file_path:sub(1,#folder):lower() == folder:lower();
	end;
	return false;
end;

function xws_mgr.on_rename_file(self, id, phase, from_path, to_path)
	-- phase
	-- 0 begin to rename, return true to do rename
	-- 1 file renamed
	-- 2 failed to rename
	local all_doc = {mgr.get_all_documents()};
	if phase == 0 then
		-- stop monitor document
		for idx, doc in ipairs(all_doc) do
			local file_name = doc.file_name;
			if is_file_in_path(file_name, from_path) then
				mgr.monitor_doc(file_name, false);
			end;
		end;
		return true;
	end;
	if phase == 1 then
		-- file named successfully
		for idx, doc in ipairs(all_doc) do
			local file_name = doc.file_name;
			if file_name == from_path then
				doc.rename(to_path);
				-- restart monitor document
				mgr.monitor_doc(to_path, true);
				lsp:did_rename_doc(doc, from_path, to_path);
				break;
			end;
			if is_file_in_path(file_name, from_path) then
				local new_path = make_path(to_path, file_name:sub(#from_path + 1));
				doc.rename(new_path);
				-- restart monitor document
				mgr.monitor_doc(new_path, true);
				lsp:did_rename_doc(doc, file_name, new_path);
			end;
		end;
	else
		for idx, doc in ipairs(all_doc) do
			local file_name = doc.file_name;
			if file_name == from_path then
				doc.rename(to_path);
				-- restart monitor document
				mgr.monitor_doc(from_path, true);
				break;
			end;
			if is_file_in_path(file_name, from_path) then
				-- restart monitor old document
				mgr.monitor_doc(file_name, true);
			end;
		end;
	end;
end;

function xws_mgr.on_load_xws(self, xws_file)
	self.is_loaded = true;
    print("load solution " .. xws_file);
	menu_bar.show_project_menu();
	mgr.hide_single_tab = false;
	edx.active("solution");
    return 1;
end;

function xws_mgr.on_unload_xws(self, xws_file)
	self.is_loaded = false;
	print("unload solution " .. xws_file);
	menu_bar.hide_project_menu();
	edx.hide("solution");
	project_configs = nil;
	CMAKE_PROJECT_FILE = nil;
	CMAKE_PROJECT_PATH = nil;
	CMAKE_PROJECT_CBP = nil;
	CMAKE_LAST_CONFIG = {};
	mgr.search_path = nil;
	mgr.hide_single_tab = true;
	edx:active("editor");
	return 1;
end;
xws_mgr._pre_load_dir = {
	function(self,dir)
		mgr:clear_doc_diagnoistic("", "");
	end;
};
xws_mgr._post_load_dir = {};
xws_mgr._post_unload_dir = {
	function(self, dir)
		edx:add_recently_folder(dir);
		mgr:clear_doc_diagnoistic("", "");
	end;
};
function xws_mgr._do_load_dir(self, dir)
	if dir:sub(-1) == "\\" or dir:sub(-1) == "/" then
		dir = dir:sub(1,-2);
	end;
	if self._pre_load_dir then
		for idx, cb in ipairs(self._pre_load_dir) do
			if cb(self, dir) then
				break;
			end;
		end;
	end;
	
	local cmake_file = make_path(dir, "CMakeLists.txt");
	if (utils:file_time(cmake_file)) then
		-- is cmake project
		CMAKE_PROJECT_FILE = cmake_file;
		CMAKE_PROJECT_PATH = dir;
		CMAKE_LAST_CONFIG = {};
		menu_bar:show_cmake_menu();
		menu_bar:show_debug_menu();
		reload_cmake_project_settings();

		local build = project_configs and project_configs[".build"];
		if build and build.config then
			menu_bar.set_cmake_config(build.config);
		end;
		if build and build.toolset then
			menu_bar.set_cmake_toolset(build.toolset);
		end;
		local toolset = menu_bar.get_cmake_toolset(true);
		if (not toolset) or toolset["target"] ~= "wsl" then
			if #CMAKE_PATHS == 0 then
				print("\x1b[33;41m", LANG("CMake not found!"), "\x1b[0m");
				print("\x1b[33;41m", LANG("Download from https://cmake.org/download"), "\x1b[0m");
			end;
			if #NINJA_PATHS == 0 then
				print("\x1b[33;41m", LANG("Ninja-Build not found!"), "\x1b[0m");
				print("\x1b[33;41m", LANG("Download from https://ninja-build.org"), "\x1b[0m");
			end;
		end;
		
		local save_project_config = false;

		if project_configs == nil then
			project_configs = empty_project_configs();
			save_project_config = true;
		end;
		
		-- load cmake targets
		local cmd = find_build_command(nil,true);
		if cmd ~= nil and cmd.type == "cmake" then

			local do_build_params = build_cmake_do_build_params(cmd, nil,function()
					local build = project_configs[".build"];
					if build and build.target then
						menu_bar.set_cmake_default_target(build.target);
					end;
					lsp:reset(cmd);
					cmake_update_include_path_for_opened_documents();

					if save_project_config then
						save_cmake_project_settings();
					end;
			end);
			CMAKE_LAST_CONFIG[cmd.build_path] = cmd.config;
			edx.do_build(do_build_params);
			__last_toolset = cmd.toolset;
			local editor = project_configs and project_configs[".editor"];
			if editor and editor.opened then
				local paths = {};
				for i,filename in ipairs(editor.opened) do
					table.insert(paths, make_path(CMAKE_PROJECT_PATH,filename));
				end;
				mgr.open_doc(paths);
			end;
		end;
	else
		lsp:_try_init_lsp(dir);
		menu_bar.show_project_menu();
	end;
	if self._post_load_dir then
		for idx, cb in ipairs(self._post_load_dir) do
			if cb(self, dir) then
				break;
			end;
		end;
	end;
end;

function xws_mgr.on_load_dir(self, dir)
	self.is_loaded = true;
	print("load dir " .. dir);
	dbg_call(function()
			self:_do_load_dir(dir);
	end,"Load dir error!");
	local code = edx.load_code_version_service(dir);
	if code == 0 then
		edx.active("revisions");
	elseif code ~= -1 then
		local error_codes = {
			[-36] = "notify/revision/error/invalid_owner";
		};
		show_notify(string.format(LANG(error_codes[code] or "notify/revision/error/can_not_error"), code, dir));
	end;
	edx.active("solution");
	mgr.search_path = dir;
	mgr.hide_single_tab = false;
	edx.default_save_path = dir;
	return 1;
end;

function xws_mgr.on_unload_dir(self, dir)
	self.is_loaded = false;
	print("unload dir " .. dir);
	edx.unload_code_version_service();
	menu_bar:hide_project_menu();
	menu_bar:hide_cmake_menu();
	menu_bar:hide_debug_menu();
	edx.hide("solution");
	edx.hide("revisions");

	if CMAKE_PROJECT_FILE ~= nil then
		if project_configs == nil then
			project_configs = empty_project_configs();
		end;
		-- close all opened project files
		local all_doc = {mgr.get_all_documents()};
		local all_doc_name = {};
		local all_doc_states = project_configs[".editor"].states or {};
		for idx, doc in ipairs(all_doc) do
			local file_name = doc.file_name;
			if file_name:sub(0,	#CMAKE_PROJECT_PATH):lower() == CMAKE_PROJECT_PATH:lower() then
				local short_name = file_name:sub(2+#CMAKE_PROJECT_PATH);
				short_name = short_name:gsub("\\", "/");
				table.insert(all_doc_name, short_name);
				all_doc_states[short_name] = {
					cursor = {doc.cursor_line;doc.cursor_column;};
					fold = doc.fold_status;
				};
			end;
		end;
		mgr.close_doc(all_doc);
		project_configs[".editor"] = {
			opened = all_doc_name;
			states = all_doc_states;
		};
		save_cmake_project_settings();
	end;
	lsp:terminate();

	project_configs = nil;
	CMAKE_PROJECT_FILE = nil;
	CMAKE_PROJECT_PATH = nil;
	CMAKE_PROJECT_CBP = nil;
	CMAKE_LAST_CONFIG = {};
	mgr.search_path = nil;
	mgr.hide_single_tab = true;
	edx.default_save_path = "";
	edx:active("editor");

	if self._post_unload_dir then
		for idx, cb in ipairs(self._post_unload_dir) do
			if cb(self, dir) then
				break;
			end;
		end;
	end;
	return 1;
end;

function xws_mgr.on_delete_file(self, id, file_path)
    -- 删除文件之前强制关闭这个文档
    mgr:remove_doc(file_path);
end;

function xws_mgr.on_cmd(self, id)
	local cmd = self._cmd_map[id];
	if cmd then
		return dbg_call(function() return cmd(self, id); end, "error");
	end;
	return 0;
end;

xws_mgr._cmd_map = {
	[_op.git_diff_file] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		if item_type == 4 or item_type == 3 or item_type == 2 then
			edx.cvs_diff_file(item_path);
		end;
		return true;
	end;
	[_op.project_configuration] = function(self, id)
		config_debug_run:show();
		return true;
	end;
};

function xws_mgr.on_create_file(self, id, file_path)
	local workspace = self.workspace;
	local is_cmake_file = ("CMakeLists.txt" == file_path:gsub("(.*[/\\])", ""));
	if is_cmake_file then
		local file_dir = make_path(file_path, "..");
		local folder_name = file_dir:match([[.*[/\]([^/\]+)]]);
		if not folder_name then
			folder_name = "NONAME";
		end;
		local file_content = string.format(CMAKE_LIST_TEMPLATE, folder_name, folder_name, "# add_link_options(<LINKER_FLAGS>)");
		if mgr:open_doc(file_path) then
			doc = mgr:get_document(file_path);
			doc:set_text(file_content);
			doc:save();
			doc:set_cursor(0,0);
		end;
		local root_cmake_path = make_path(workspace, "CMakeLists.txt");
		if file_path == root_cmake_path then
			-- 转换工程为CMake工程
			menu_bar:hide_project_menu();
			self:_do_load_dir(workspace);
		end;
	end;
end;

function xws_mgr.on_create_dir(self, id, dir)
end;

-- 更新最近使用的文件列表
function edx.reload_recently_menu(self)
	local env_time = utils:file_time(edx_env_path);
	if env_time <= self.__env_time then
		return false;
	end;
	local rf = io.open(edx_env_path);
	if rf ~= nil then
		self.__env_time = utils:file_time(edx_env_path);
		local new_env = utils.fromjson(rf:read());
		self.env.recently = new_env.recently;
		self.env.recently_folders = new_env.recently_folders;
		rf:close();
		return true;
	end;
	return false;
end;
function edx.update_recently_menu(self)
    local recently_docs = self.env.recently;
	local recently_folders = self.env.recently_folders;
    local file_menu = menu_bar.sub_menu(0x8000001, false);
	file_menu.remove_item(0x8100001, false);
	file_menu.remove_item(0x8100002, false);
	file_menu.remove_item(0x8200001, false);
	if sizeof(recently_docs) ~= 0 or sizeof(recently_folders) ~= 0 then
		file_menu.insert_line(0x8000000, 0x8200001, false);
	end;
    if sizeof(recently_docs) ~= 0 then
		file_menu.insert_item(0x8000000, LANG("menu_bar/file/recently_files"), 0x8100001, false);
        local recently_menu = file_menu.sub_menu(0x8100001, false);
		for idx, name in pairs(recently_docs) do
			local item_id = recently_menu.insert_item(-1, name, RECENTLY_ITEM_ID_BASE + tonumber(idx), true);
			recently_menu.set_accept_mbtn(item_id, true, false);
        end;
    end ;
	if sizeof(recently_folders) ~= 0 then
		file_menu.insert_item(0x8000000, LANG("menu_bar/file/recently_folders"), 0x8100002, false);
		local recently_menu = file_menu.sub_menu(0x8100002, false);
		for idx, name in pairs(recently_folders) do
			local item_id = recently_menu.insert_item(-1, name, RECENTLY_FOLDER_ID_BASE + tonumber(idx), true);
			recently_menu.set_accept_mbtn(item_id, true, false);
        end;
    end;
end;
function edx.add_recently_doc(self, doc_name, is_remove)
	self:reload_recently_menu();
    doc_name = doc_name:lower();
    if doc_name == "" or doc_name == nil then
        return ;
    end ;
	local new_recently_docs = {};
	local counter = 0;
	if not is_remove then
		counter = 1;
		new_recently_docs[counter] = doc_name;
	end;
    local recently_docs = self.env.recently;
    self.env.recently = new_recently_docs;
    for idx, name in pairs(recently_docs) do
        if name ~= doc_name then
			counter = counter + 1;
			new_recently_docs[counter] = name;
        end;
		if counter >= 16 then
            break;
        end;
	end;
	self:update_recently_menu();
	self:save_env();
end;
function edx.add_recently_folder(self, folder_path, is_remove)
	self:reload_recently_menu();
	folder_path = folder_path:lower();
	if folder_path == "" or folder_path == nil then
        return ;
    end ;
	local new_recently_folders = {};
	local counter = 0;
	if not is_remove then
		counter = 1;
		new_recently_folders[counter] = folder_path;
	end;
	local recently_folders = self.env.recently_folders;
	self.env.recently_folders = new_recently_folders;
	for idx, name in pairs(recently_folders) do
		if name ~= folder_path then
			counter = counter + 1;
			new_recently_folders[counter] = name;
        end;
		if counter >= 16 then
            break;
        end;
    end;
	self:update_recently_menu();
	self:save_env();
end;
function edx.remove_recently_folder(self, folder_path)
	self:add_recently_folder(folder_path, true);
end;
function edx.remove_recently_doc(self, doc_name)
	self:add_recently_doc(doc_name, true);
end;
-- 加载EDX环境
function edx.load_env(self)
    local rf = io.open(edx_env_path);
	if rf ~= nil then
		self.__env_time = utils:file_time(edx_env_path);
		self.env = utils.fromjson(rf:read("a"));
        rf:close();
    end;
    -- 更新最近使用的文件列表菜单
    self:update_recently_menu();

    local main = self.env.main;
    if main then
        -- 恢复上一次的窗口大小
        self:set_position(main.position.left, main.position.top, main.position.width, main.position.height);
        if main.show == 2 then
            self.show_status = main.show;
        end ;
    end ;
end;
-- 保存EDX环境
function edx.save_env(self)
    self:switch_layout(nil);
    local position = { self:get_normal_position() };
    self.env.main = {
        position = {
            left = position[1];
            top = position[2];
            width = position[3];
            height = position[4];
        };
        show = self.show_status;
    };
    local rf = io.open(edx_env_path, "w+");
    rf:write(utils.tojson(self.env));
	rf:close();
	self.__env_time = utils:file_time(edx_env_path);
end;

function edx.load_toolset(self)
	local rf = io.open(toolset_env_path);
	if rf ~= nil then
		local custom_toolsets = rf:read("a");
		if custom_toolsets then
			custom_toolsets = utils.fromjson(custom_toolsets);
		end;
		update_custom_toolsets(custom_toolsets);
	end;
end;

function edx.save_toolset(self)
	local custom_toolsets = collect_custom_toolsets();
	local rf = io.open(toolset_env_path, "w+");
	rf:write(utils.tojson(custom_toolsets));
	rf:close();
end;

-- 切换docking布局, layout 可以是 simple, debug
function edx.switch_layout(self, layout)
    if self.last_layout ~= nil then
        -- 保存当前的layout
        self.env.layout[self.last_layout] = self.layout;
    end ;
    self.last_layout = layout;
    if self.env.layout[self.last_layout] == nil then
        return false;
	end ;
	local new_layout = self.env.layout[self.last_layout];
	local pane_name_map = {
		["Watch"] = "watch";
		["Locals"] = "locals";
		["Call Stack"] = "stack";
		["Threads"] = "threads";
		["Modules"] = "modules";
		["Registers"] = "registers";
		["Debugger"] = "debugger";
		["Output"] = "output";
		["Find & Replace"] = "find";
		["Build Results"] = "build_result";
		["LSP"] = "lsp";
		["Solution Explorer"] = "solution";
		["Class View"] = "class";
		["Resource View"] = "resource";
		["Property"] = "properties";
		["Structure View"] = "structure";
		["Revisions"] = "revisions";
	};
	-- fix old layout data
	new_layout = new_layout:gsub([[>([%d%s%w]-)</]],
		function(val)
			local name = pane_name_map[val];
			if name then
				return ">" .. name .. "</";
			end;
			return ">" .. val .. "</";
		end
	);
	self.layout = new_layout;
end;

mgr.completion_dispatcher = {
};

require "cmake_completion"
require "clangd_completion"
require "lua_completion"
require "java_completion"

mgr.completion_dispatcher = {
	[content_type_cxx] = "on_cxx_completion";
	[content_type_dlang] = "on_cxx_completion";
	[content_type_lua] = "on_lua_completion";
	[content_type_java] = "on_cxx_completion";
	[content_type_cmake] = "on_cmake_completion";
};

function mgr:on_completion(status, helper)
    local doc = mgr.current_document;
    -- print( "complete ", status, " doc type ", doc.content_type );
	local dispatcher = self.completion_dispatcher[doc.content_type];
	dispatcher = self[dispatcher];
	if not dispatcher then
		return 0;
	end;
	return dispatcher(self, doc, status, helper);
end;

function mgr:on_completion_tip(status, helper)
	local doc = mgr.current_document;
	if status == 0 then
		return lsp:find_signature_help(doc, helper);
	end;
	if status == 2 then
		local txt = "";
		if mgr._last_column ~= nil then
			if mgr._last_column < doc.cursor_column then
				txt = doc.get_text(doc.cursor_line, doc.cursor_column-1, 1);
			elseif mgr._last_column > doc.cursor_column then
				txt = doc.get_text(doc.cursor_line, doc.cursor_column, 1);
			end
		end;
		mgr._last_column = doc.cursor_column;
		if txt == "," or txt == "(" or txt == ")" then
			lsp:find_signature_help(doc, helper);	
		end;
	end;
	return 1;
end;

function mgr:on_locate_file(from, file_name, begin_line, begin_col, end_line, end_col)
	mgr.record_cursor_history();
	if file_name:match([[^\\wsl%$\.*$]]) or utils:file_time(file_name) then
		-- keep the wsl path
	elseif __last_toolset and __last_toolset.unmap_path then
		local unmap_path = __last_toolset.unmap_path;
		file_name = unmap_path(file_name);
	end;
	if mgr.open_doc(file_name) then
		local cur_doc = mgr.current_document;
		cur_doc:set_cursor(begin_line, begin_col);
		cur_doc:select_text(begin_line, begin_col, end_line, end_col);
		edx.do_cmd(_op.center_cursor_line);
	else
		print("failed to locate_file ", file_name, " ", begin_line, ",", begin_col, ",", end_line, ",", end_col);
	end;
end;

function mgr:on_closing(doc)
	local doc_name = doc.file_name:lower();
	if doc_name == "" or doc_name == nil then
		return ;
	end ;
	edx:add_recently_doc(doc_name);
	if doc.doc_type ~= doc_type.text then
		-- not a text file
		return;
	end;
	lsp:doc_closed(doc);
	edx:store_breakpoints(doc)
	edx:store_file_states(doc)
end;

function mgr:on_opened(doc)
	if doc.doc_type ~= doc_type.text then
		-- not a text file
		return;
	end;
	lsp:doc_opened(doc);
	lsp:list_doc_structure(doc, nil); -- preload the document structure

	local def_target = menu_bar.get_cmake_default_target();
	if def_target ~= nil and def_target.include ~= nil then
		doc.include_path = table.concat(def_target.include, ";");
	end;
	edx:restore_breakpoints(doc)
	edx:restore_file_states(doc)
end;

function mgr:on_saved(doc)
	lsp:doc_saved(doc);
	if doc.file_name == edx_config_path then
		edx:reload_config();
	end;
end;

function mgr:on_char(c)
    table.insert(self.__last_cmd, 1, 0);
	local doc = self.current_document;

    -- C++ 自动补全
	if doc.content_type == content_type_cxx then
		if (c == "\"" or c == "<" or c == ">" or c == "." or c == ":" or c == "#" or c == "(" or c == ")" or c == "," or c == ";") then
            doc.write_char(c);
            if c == ":" then
				edx.do_cmd(_op.format_selection);
			elseif c == ")" or c == "," then
				edx.do_cmd(_op.update_completion_tip);
				edx.do_cmd(_op.update_completion_list);
				return true;
			elseif c == ";" then
				edx.do_cmd(_op.completion_end_tip);
				edx.do_cmd(_op.completion_end_list);
				return true;
			end;
			
			local text = doc.get_text(doc.cursor_line, 0, doc.cursor_column);
            if text ~= nil then
                local match_text = text:match("^[ \t]*#[ \t]*include[ \t]+[\"<]$");
                if match_text ~= nil then
                    -- 自动补全include
                    edx.do_cmd(_op.show_completion_list);
                end ;
                local match_text = text:match("^.*::$");
                if match_text ~= nil then
                    -- 自动补全xxx::
                    edx.do_cmd(_op.show_completion_list);
                end ;
                local match_text = text:match("^.*%-%>$");
                if match_text ~= nil then
                    -- 自动补全xxx->
                    edx.do_cmd(_op.show_completion_list);
                end;
				local match_text = text:match("^.*[a-zA-Z_][a-zA-Z_0-9]*%s*[%(]$");
				if match_text ~= nil then
					-- 自动补全xxx(
					edx.do_cmd(_op.show_completion_tip);
					edx.do_cmd(_op.update_completion_list);
					return 1;
				end;
				local match_text = text:match("^.*%.%.$");
				if match_text ~= nil then
					-- skip ..
					return 1;
				end;
                local match_text = text:match("^.*%.$");
                if match_text ~= nil then
                    -- 自动补全xxx.
                    edx.do_cmd(_op.show_completion_list);
                end;
                local match_text = text:match("^[ \t]*#[ \t]*$");
                if match_text ~= nil then
                    -- 自动补全#
                    edx.do_cmd(_op.show_completion_list);
                end ;
            end ;
            return 1;
        end ;
    end ;
    -- LUA自动格式化
    if c == ';' and doc.content_type == content_type_lua then
        self.current_document.write_char(c);
        edx.do_cmd(_op.format_selection);
        return 1;
    end ;
    -- XML HTML自动格式化
    if doc.content_type == content_type_html or doc.content_type == content_type_xml then
        if c == '>' then
            self.current_document.write_char(c);
            edx.do_cmd(_op.format_selection);
            return 1;
        end ;
    end ;
    return 0;
end;

function mgr:on_active(doc)
	local cmd = find_build_command();
	if cmd and cmd.debug_type then
		menu_bar:show_debug_menu();
	else
		menu_bar:hide_debug_menu();
	end;
	
end;

function mgr:on_tab_event(event, doc)
	if event == "rbtn_clicked" then
		edx.__last_tab_doc = doc.file_name;
		local tab_menu = edx.tab_menu;
		if tab_menu then
			tab_menu.enable_item(_op.locate_tab_file, false, xws_mgr.is_loaded);
		end;
	else
		edx.__last_tab_doc = nil;
	end;
	-- print("tab event: ", event, ", ", doc);
	return 0;
end;

function mgr:on_mouse_hover(doc, x, y, line, col)
	local dbg = edx.dbg;
	if dbg and dbg:is_running() then
		dbg:hover_info(doc, line, col, dbg.beautifier);
		return;
	end;
	lsp:hover_info(doc, line, col);
end;

function edx:on_exit()
	edx:save_env();
	edx:save_toolset();
	lsp:terminate();
end;

function edx:on_popup_menu(menu, owner_menu, owner_item_id)
	-- print(string.format("owner:%x", owner_item_id));
	if owner_menu then
		-- POPUP SUBMENU
		if 0xA100004 == owner_item_id or 0xA000001 == owner_item_id then
			-- update serial ports
			menu_bar.update_cmake_serial_port();
		end;
		if 0x8000001 == owner_item_id then
			if self:reload_recently_menu() then
				self:update_recently_menu();
			end;
		end;
	else
		-- POPUP MENU
	end;
end;

edx.command_handlers={
	["output"] = {
		["reset"] = function(self, cmd, target)
			if target == "lsp" then
				print("Reseting LSP...");
				local cmd = find_build_command(nil, true);
				lsp:reset(cmd);
				return 1;
			end;
			return 0;
		end;
		["clear"] = function(self, cmd, target)
			if target == nil then
				target = "output";
			end;
			local valid_targets = {
				["output"] = true;
				["find"] = true;
				["lsp"] = true;
				["debugger"] = true;
				["serial_ports"] = true;
				["build_result"] = true;
			};
			if valid_targets[target] == nil then
				print("clear [", table.concat(table.keys(valid_targets), "|"), "]");
				return 1;
			end;
			self:clear(target);
			return 1;
		end;
		["debug"] = function(self, cmd, dbg_type, target, arguments, cwd, envs)
			if dbg_type == nil then
				print("debug [windbg|gdb] {target} [arguments] [working path]");
				return 1;
			end;
			if dbg_type ~= "windbg" and dbg_type ~= "gdb" then
				envs = cwd;
				cwd = arguments;
				arguments = target;
				target = dbg_type;
				dbg_type = "windbg";
			end;
			debug_program(target, dbg_type, arguments, cwd, envs);
			return 1;
		end;
		["gdb"] = function(self, cmd, target, arguments, cwd, envs)
			if target == nil then
				print("gdb {target} [arguments] [working path]");
				return 1;
			end;
			debug_program(target, "gdb/mi", arguments, cwd, envs);
			return 1;
		end;
		["windbg"] = function(self, cmd, target, arguments, cwd, envs)
			if target == nil then
				print("windbg {target} [arguments] [working path]");
				return 1;
			end;
			debug_program(target, "windbg", arguments, cwd, envs);
			return 1;
		end;
		["syntax"] = function(self, cmd, typename)
			local doc = mgr.current_document;
			if typename == nil then
				if doc.doc_type == doc_type.hex then
					print("HEX file");
				elseif doc.doc_type == doc_type.text then
					typename = content_type_name[doc.content_type];
					if typename then
						print("Current syntax type is:", typename);
					else
						print("Unknown text file(", doc.content_type, ")");
					end;
				end;
			else
				if typename == "off" then
					typename = "text";
				end;
				if typename ~= "?" then
					for type_id, name in pairs(content_type_name) do
						if name == typename then
							doc.content_type = type_id;
							print("Switch to ", typename, " mode.");
							return 1;
						end;
					end;
					print("Unknown syntax type: ", typename);
				end;
				local name_map = {};
				local name_array = {};
				for type_id, name in pairs(content_type_name) do
					if name_map[name] == nil then
						table.insert(name_array, name);
						name_map[name] = name;
					end;
				end;
				print("Available syntax types:", table.concat(name_array, "|"));
			end;
			return 1;
		end;
		["build"] = function(self, cmd, work)
			if work == nil then
				self:on_cmd(_op.build_target);
				return 1;
			end;
			if work == "project" then
				self:on_cmd(_op.build_project);
				return 1;
			end;
			return 0;
		end;
		["rebuild"] = function(self, cmd, work)
			if work == nil then
				self:on_cmd(_op.build_target);
				return 1;
			end;
			if work == "project" then
				self:on_cmd(_op.rebuild_project);
				return 1;
			end;
			return 0;
		end;
		["stop"] = function(self, cmd, work)
			if work == "build" then
				self:on_cmd(_op.stop_build);
				return 1;
			end;
			return 0;
		end;
	};
};
function edx:on_handle_command(source, args)
	local handler = edx.command_handlers[source];
	handler = handler and handler[args[1]];
	if handler then
		return dbg_call(function() return handler(self, table.unpack(args)); end, "on_handler_command:");
	end;
	return 0;
end;

local function update_serial_ports()
	local ports = {utils:list_serial_ports()};
	table.sort(ports, function(a, b)
			return tonumber(a[1]:sub(4)) < tonumber(b[1]:sub(4));
	end);
	edx:update_serial_ports(ports);
end;

function edx:on_active_pane(pane)
	if pane == "serial_ports" then
		update_serial_ports();
	end;
end;

update_serial_ports();
edx:update_serial_baudrate(SERIAL_BAUDRATES, SERIAL_DEFAULT_BAUDRATE);

function edx_do_build(message, do_build_params)
	mgr:clear_doc_diagnoistic("compiler", "");
	local ret = edx.do_build(do_build_params);
	if ret then
		edx.clear("output");
		edx.clear("build_result");
		print(message);
		edx.active("output");
	elseif ret == nil then
		show_notify(LANG("notify/build/still_running"));
	else
		show_notify(LANG("notify/build/failed"));
	end;
end;

global_cmd_map = {
	[_op.xws_open_file_dir] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			edx:shell_execute("open", "explorer.exe", [[/select,"]]..item_path..[["]]);
		else
			edx:shell_execute("open", open_path, "");
		end;
	end,
	[_op.xws_open_terminal] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			-- file item
			open_path = make_path(item_path, "../");
		end
		if ESP32_IDF_CMAKE_PROJECT then
			esp32_idf_show_command_line(open_path);
		else
			open_terminal(open_path,true);
		end;
	end;
	[_op.ext_git_open_gui] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			-- file item
			open_path = make_path(item_path, "../");
		end
		if open_path == "" then
			open_path = xws_mgr.workspace;
		end;
		edx.shell2([[git gui]], "", open_path);
	end;
	[_op.ext_git_open_history] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			-- file item
			open_path = make_path(item_path, "../");
		end
		if open_path == "" then
			open_path = xws_mgr.workspace;
		end;
		edx.shell2([[gitk]], "", open_path);
	end;
	[_op.ext_git_pull] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			-- file item
			open_path = make_path(item_path, "../");
		end
		if open_path == "" then
			open_path = xws_mgr.workspace;
		end;
		edx:clear("output");
		edx.shell2([[git pull --autostash --progress]], "output", open_path);
		edx:active("output");
	end;
	[_op.ext_git_push] = function(self, id)
		local item_path, item_type = xws_mgr.get_selected_item();
		local open_path = item_path;
		if item_type == 4 then
			-- file item
			open_path = make_path(item_path, "../");
		end
		if open_path == "" then
			open_path = xws_mgr.workspace;
		end;
		edx:clear("output");
		edx.shell2([[git push --progress]], "output", open_path);
		edx:active("output");
	end;
	[_op.xws_close_opend_file] = function(self,id)
		local item_path, item_type = xws_mgr.get_selected_item();
		if item_type == 4 then
			mgr.close_doc(item_path);
		else
			local all_docs = {mgr.get_all_documents()};
			local len = #item_path;
			for i, doc in ipairs(all_docs) do
				local doc_file = doc.get_file_name();
				if doc_file:sub(0,len) == item_path then
					mgr.close_doc(doc_file);
				end;
			end;
		end;
		mgr.current_document:redraw();
	end;
	[_op.xws_rename_selected_item] = function(self,id)
		xws_mgr:rename_selected_item();
	end;
	[_op.xws_new_file] = function(self, id)
		-- create file
		xws_mgr:create_item(4, true);
	end;
	[_op.xws_new_folder] = function(self, id)
		-- create folder
		xws_mgr:create_item(3, false);
	end;
	[RECENTLY_ITEM_ID_BASE] = function(self, id, flags)
		id = id - RECENTLY_ITEM_ID_BASE;
		local recently = edx.env.recently;
		local doc_name = recently[id];
		if doc_name == nil then
			doc_name = recently[tostring(id)];
		end;
		if utils:file_time(doc_name) == nil then
			self:remove_recently_doc(doc_name);
			show_notify(string.format(LANG("notify/file_not_found"), doc_name));
			return 1;
		end;
		if flags == 1 or (self:get_key_status("shift") and not self:get_key_status("alt") and not self:get_key_status("ctrl")) then
			-- open file in new window
			local cmd = string.format("\"%s\" -new-window \"%s\"", make_path(self.app_path, "edx.exe"), doc_name);
			self:shell2(cmd, "*", self.startup_path);
			return 1;
		end;
		mgr:open_doc(doc_name);
		return 1;
	end;
	[RECENTLY_FOLDER_ID_BASE] = function(self, id, flags)
		id = id - RECENTLY_FOLDER_ID_BASE;
		local recently = edx.env.recently_folders;
		local doc_name = recently[id];
		if doc_name == nil then
			doc_name = recently[tostring(id)];
		end;
		if utils:file_time(doc_name) == nil then
			self:remove_recently_folder(doc_name);
			show_notify(string.format(LANG("notify/folder_not_found"), doc_name));
			return 1;
		end;
		if flags == 1 or (self:get_key_status("shift") and not self:get_key_status("alt") and not self:get_key_status("ctrl")) then
			-- open dir in new window
			local cmd = string.format("\"%s\" -new-window \"%s\"", make_path(self.app_path, "edx.exe"), doc_name);
			self:shell2(cmd, "*", self.startup_path);
			return 1;
		end;
		lsp:terminate();
		self:stop_build();
		menu_bar:enable_cmake_menu_items();
		xws_mgr:load_xws(doc_name);
		return 1;
	end;
	-- CMAKE targets
	[CMAKE_TARGETS_ID_BASE] = function(self, id)
		menu_bar.set_cmake_default_target(id);
		cmake_update_include_path_for_opened_documents();
		-- clean up
		return 1;
	end;
	-- CMAKE toolsets
	[CMAKE_TOOLSET_ID_BASE] = function(self, id)
		if menu_bar.set_cmake_toolset(id) then
			self:on_cmd(_op.cmake_build_cache);
		end;
		return 1;
	end;
	[CMAKE_SERIAL_PORTS_ID_BASE] = function(self, id)
		menu_bar.set_cmake_serial_port(id);
		return 1;
	end;
	[CMAKE_SERIAL_BUAD_ID_BASE] = function(self, id)
		menu_bar.set_cmake_serial_baud(id);
		return 1;
	end;
	[CMAKE_CPU_TYPES_ID_BASE] = function(self, id)
		menu_bar.set_cmake_cpu(id);
		menu_bar:disable_cmake_menu_items();
		lsp:terminate();
		esp32_idf_set_target(menu_bar.get_cmake_cpu(), menu_bar.get_cmake_config(), function(success)
				menu_bar:enable_cmake_menu_items();
				if success then
					local cmd = find_build_command(nil, true);
					lsp:reset(cmd);
					cmake_reload_cbp(cmd);
					cmake_update_include_path_for_opened_documents();
				end;
		end);
		return 1;
	end;
	[CMAKE_BOARD_TYPES_ID_BASE] = function(self, id)
		menu_bar.set_cmake_board(id);
		return 1;
	end;
	[CMAKE_DEBUG_ADAPTERS_ID_BASE] = function(self, id)
		menu_bar.set_cmake_debug_adapter(id);
		return 1;
	end;
	-- CMAKE config
	[CMAKE_CONFIG_ID_BASE] = function(self, id)
		if menu_bar.set_cmake_config(id) then
			self:on_cmd(_op.cmake_build_cache);
		end;
		return 1;
	end;
	-- CMAKE install
	[_op.cmake_install] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command("install", true);
		if cmd ~= nil and cmd.type == "cmake" then
			local do_build_params = build_cmake_do_build_params(cmd, "build_target", show_install_result_notification);

			__last_toolset = cmd.toolset;
			edx_do_build("Build & Install CMake project", do_build_params);
			self.active("output");
		end
		return 1;
	end;
	[_op.cmake_build_cache] = function(self)
		reload_cmake_project_settings();
		local cmd = find_build_command(nil, true);
		if cmd ~= nil and cmd.type == "cmake" then
			local do_build_params = build_cmake_do_build_params(cmd, nil, function(success)
					local cmd = find_build_command(nil, true);
					lsp:reset(cmd);
					cmake_update_include_path_for_opened_documents();
			end);

			__last_toolset = cmd.toolset;
			edx_do_build("Update CMake cache", do_build_params);
		end
		self:active("output");
		return 1;
	end;
	[_op.cmake_rebuild_cache] = function(self)
		local cmd = find_build_command(nil, true);
		if cmd ~= nil and cmd.type == "cmake" then
			-- use local path for WSL target
			local map_path = (cmd.toolset and (not cmd.toolset.wsl) and cmd.toolset.map_path) or function(path) return path; end;
			local do_build_params = build_cmake_do_build_params(cmd, nil, function(success)
					local cmd = find_build_command(nil, true);
					lsp:reset(cmd);
					cmake_update_include_path_for_opened_documents();
					if success then
						show_notify(LANG("notify/build/cmake_cache_rebuilt"));
					else
						show_notify(LANG("notify/build/cmake_cache_rebuild_failed"));
					end;
				end,
				{"@DELETE \"" .. map_path(cmd.build_path) .. "\""}
			);

			__last_toolset = cmd.toolset;
			edx_do_build("Rebuild CMake cache", do_build_params);
			self.active("output");
		end
	end;
	[_op.cmake_refresh_cache] = function(self)
		local cmd = find_build_command(nil, true);
		if cmd ~= nil and cmd.type == "cmake" then
			local do_build_params = build_cmake_do_build_params(cmd, nil, function(success)
					local cmd = find_build_command(nil, true);
					lsp:reset(cmd);
					cmake_update_include_path_for_opened_documents();
					if success then
						show_notify(LANG("notify/build/cmake_cache_refreshed"));
					else
						show_notify(LANG("notify/build/cmake_cache_refresh_failed"));
					end;
			end);

			__last_toolset = cmd.toolset;
			edx_do_build("Rebuild CMake cache", do_build_params);
			self.active("output");
		end
	end;
	-- 编译整个工程
	[_op.build_project] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command();
		if cmd ~= nil then
			local do_build_params;
			if cmd.type == "cmake" then
				do_build_params = build_cmake_do_build_params(cmd, "build", show_build_result_notification);
			else
				do_build_params = build_do_build_params(cmd, "build", show_build_result_notification);
			end;

			__last_toolset = cmd.toolset;
			edx_do_build("Build project", do_build_params);
			return 1;
		end
		self.clear("output");
		self.shell("mingw32-make all", "output");
		self.active("output");
		return 1;
	end; -- 编译单个目标
	[_op.build_target] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command();
		local do_build_params;
		if cmd ~= nil then
			if cmd.type == "cmake" then
				do_build_params = build_cmake_do_build_params(cmd, "build_target", show_build_result_notification);
			else
				do_build_params = build_do_build_params(cmd, "build", show_build_result_notification);
			end;

			__last_toolset = cmd.toolset;
			edx_do_build("Build target", do_build_params);
			self.active("output");
			return 1;
		end;
		
		self.clear("output");
		self.shell("mingw32-make all", "output");
		self.active("output");
		return 1;
	end;
	[_op.build_and_run] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command();
		if cmd ~= nil then
			local do_build_params;
			local on_complete = function(success)
				if success then
					local toolset = cmd.toolset;
					local wrap_command = toolset.wrap_command or function(cmd) return cmd; end;
					local map_path = toolset.map_path or function(path) return path; end;
					local file_type, file_type_name = edx:guess_file_type(cmd.target_output);
					local target_path = map_path(cmd.target_output);
					local cwd = (cmd.cwd or cmd.project_path);
					local target_cmd = target_path.. " " .. (cmd.argument or "");
					local target_env = cmd.environment;
					target_cmd = wrap_command(target_cmd);
					if target_env then
						cwd = table.clone(target_env, {[1] = cwd});
					end;
					if not (file_type_name and string.match(file_type_name, [[.*GUI EXE$]])) then
						-- for non-GUI executable target
						target_cmd = [[cmd /d /c "]] .. target_cmd:gsub("\"", [["""]]) .. [[" & pause]];
					end;
					edx:shell2(target_cmd, "*", cwd);
				else
					show_notify(LANG("notify/build/failed"));
				end;
			end;
			if cmd.type == "cmake" then
				do_build_params = build_cmake_do_build_params(cmd, "build_target", on_complete);
			else
				do_build_params = build_do_build_params(cmd, "build", on_complete);
			end;

			__last_toolset = cmd.toolset;
			edx_do_build("Build & Run target", do_build_params);
		else
			self.clear("output");
			self.shell("mingw32-make all -j2 & mingw32-make run", "output");
			self.active("output");
		end;
		return 1;
	end;
	[_op.rebuild_project] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command();
		if cmd ~= nil then
			if cmd.type == "cmake" then
				local do_build_params = build_cmake_do_build_params(cmd, "rebuild", show_build_result_notification);

				__last_toolset = cmd.toolset;
				edx_do_build("Rebuild CMake project", do_build_params);
			end;
			self.active("output");
			return 1;
		end;

		self.clear("output");
		self.shell("mingw32-make rebuild -j2", "output");
		self.active("output");
		return 1;
	end;
	[_op.rebuild_target] = function(self, id)
		reload_cmake_project_settings();
		local cmd = find_build_command();
		if cmd ~= nil then
			if cmd.type == "cmake" then
				local do_build_params = build_cmake_do_build_params(cmd, "rebuild_target", show_build_result_notification);

				__last_toolset = cmd.toolset;
				edx_do_build("Rebuild CMake target", do_build_params);
			end;
			self.active("output");
			return 1;
		end;

		self.clear("output");
		self.shell("mingw32-make rebuild -j2", "output");
		self.active("output");
		return 1;
	end;
	[_op.stop_build] = function(self, id)
		print("Stop build");
		self:stop_build();
		menu_bar:enable_cmake_menu_items();
	end;
	
	[_op.search_everywhere] = function(self, id)
		print("search_everywhere");
		edx:search_everywhere();
	end;

	-- 列举所有断点
	[_op.debug_list_break_point] = function(self, id)
		list_breakpoints();
		return 0;
	end;
	-- 列举所有工具链
	[_op.active_toolsets] = function(self, id)
		toolset_manager:show();
		return 0;
	end;
	[_op.register_shell_menu_shortcuts] = function(self, id)
		edx:register_shell_menu();
	end;

	[_op.open_config_file] = function(self,id)
		mgr.open_doc(edx_config_path);
	end;

	[_op.locate_current_document] = function(self,id)
		if xws_mgr.is_loaded then
			local cur_doc = mgr.current_document;
			xws_mgr:select_item(cur_doc.file_name);
			edx:active("solution");
		else
			show_notify(LANG("notify/project_not_loaded"));
		end;
	end;

	[_op.save_all] = function(self,id)
		mgr:save_all();
	end;

	[_op.open_homepage] = function(self, id)
		edx:shell_execute("open", "https://www.ed-x.cc", "");
	end;

	[_op.zoom_in] = function(self, id)
		mgr.scalar = mgr.scalar * 1.1;
	end;
	[_op.zoom_out] = function(self, id)
		mgr.scalar = mgr.scalar * 0.9;
	end;
	[_op.zoom_reset] = function(self, id)
		mgr.scalar = 1.0;
	end;

	[_op.serial_monitor] = function(self, id)
		esp32_idf_show_serial_monitor();
	end;

	[_op.new_project] = function(self, id)
		self:create_project();
	end;
};

function edx:on_cmd(id, flags)
	return dbg_call(
		function()
			local item;
			local op_id = id;
			if id >= _op.active_pane_first and id <= _op.active_pane_last then
				-- active/hide panes
				item = global_cmd_map[id];
				if item then
					item(self, op_id, flags);
				else
					edx:active_pane(op_id);
				end;
				return 0;
			end;
			if id >= 0xE000 and id <= 0xFFFF then
				id = id - (id % 0x20);
			else
				item = id;
			end;
			item = global_cmd_map[id];
			-- fallback to previous command handler
			while id >= 0xE000 and item == nil do
				id = id - 0x20;
				item = global_cmd_map[id];
			end;
			if item ~= nil then
				return item(self, op_id, flags);
			end;
			return 0;
		end
	);
end;

function edx:msdn_search(keyword, devlang)
    if devlang ~= nil then
		if devlang == "cmake" then
			local cmake_keys = {};
			for i, key in ipairs(cmake_completion[0]) do
				cmake_keys[key] = true;
			end

			local url;
			if cmake_keys[keyword:lower()] then
				url = ([[https://cmake.org/cmake/help/latest/command/%s.html]]):format(keyword:lower());
			else
				-- url = [[https://cmake.org/cmake/help/latest/index.html]];
				url = ([[https://cmake.org/cmake/help/latest/search.html?q=%s]]):format(keyword);
			end;
			self:shell_execute("open", url, "");
			return;
		end;
        if devlang == "javascript" then
            devlang = [[&DevLang=jscript&DevLang=dhtml&DevLang=javascript]];
        else
            devlang = ([[&DevLang=%s]]):format(devlang);
        end ;
    else
        devlang = "";
    end ;
    local help_2x_path = [[C:\Program Files (x86)\Microsoft Help Viewer\v2.1\HlpViewer.exe]];
    if utils:file_time(help_2x_path) then
        local text = ([[/catalogName VisualStudio12 /helpQuery "method=f1&query=%s%s&LCID=2502" /locale zh-CN /launchingApp Microsoft,VisualStudio,12.0]]):format(keyword, devlang);
        self:shell_execute("open", help_2x_path, text);
    else
		--local text = ([["ms-xhelp:///?method=search&product=VS&productVersion=100&query=%s&locale=en-US&DevLang=C++&TargetOS=Windows"]]):format(keyword);
		local text = ([["https://docs.microsoft.com/en-us/search/?terms=%s%s"]]):format(keyword,"&category=Documentation&products=%2Fdevrel%2F4628cbd9-6f47-4ae1-b371-d34636609eaf%2C%2Fdevrel%2Fbcbcbad5-4208-4783-8035-8481272c98b8");
        self:shell_execute("open", text, "");
    end ;
end;

text_cmd_map = {
	[_op.help] = function(self, id)
		local doc = mgr.current_document;
		local devlang = nil;
		if doc.content_type == content_type_cxx then
			-- C/C++文档
			devlang = "C++";
		elseif doc.content_type == content_type_js then
			-- JavaScript文档
			devlang = "javascript";
		elseif doc.content_type == content_type_cmake then
			-- JavaScript文档
			devlang = "cmake";
		elseif doc.content_type == content_type_lua then
			devlang = "lua";
		else
			return 0;
		end ;
		local selection = { doc.get_selection() };
		if selection[1] == nil or selection[1] ~= selection[3] then
			edx.do_cmd(_op.select_word);
			selection = { doc.get_selection() };
		end ;
		if selection[1] == nil then
			return 0;
		end ;
		local text = doc.get_text(selection[1], selection[2], selection[4] - selection[2]);
		edx:msdn_search(text, devlang);
	end;
	[_op.help_search] = function(self, id)
		local doc = mgr.current_document;
		local devlang = nil;
		if doc.content_type == content_type_cxx then
			-- C/C++文档
			devlang = "C++";
		elseif doc.content_type == content_type_js then
			-- JavaScript文档
			devlang = "javascript";
		else
			return 0;
		end ;
		local selection = { doc.get_selection() };
		if selection[1] == nil or selection[1] ~= selection[3] then
			edx.do_cmd(_op.select_word);
			selection = { doc.get_selection() };
		end ;
		if selection[1] == nil then
			return 0;
		end ;
		local text = doc.get_text(selection[1], selection[2], selection[4] - selection[2]);
		edx:msdn_search(text, devlang);
	end;
	[_op.paste] = function(self, id)
		-- 如果粘贴的行数小于100行,则自动格式化这些行
		local fl = self.current_document.cursor_line;
		local fp = self.current_document.cursor_column;
		edx.do_cmd(_op.paste_ex);
		local el = self.current_document.cursor_line;
		local ep = self.current_document.cursor_column;
		if math.abs(el - fl) < 100 then
			edx.do_cmd(_op.format_selection);
		else
			edx.do_cmd(_op.unselect);
		end ;
		return 1;
	end;
	[_op.indent_split_line_or_jump_placeholder] = function(self, id)
		if lsp:jump_placeholder(self.current_document) then
			return 1;
		end;
		edx:do_cmd(_op.indent_split_line);
		return 1;
	end;
	[0x01003] = function(self, id)
		local doc = mgr.current_document;
		local sel_text = doc.selection_text;
		if sel_text ~= nil then
			local new_text = sel_text:upper();
			if new_text ~= sel_text then
				doc.replace(new_text, true);
			end ;
		end ;
		return 1;
	end;
	[0x01004] = function(self, id)
		local doc = mgr.current_document;
		local sel_text = doc.selection_text;
		if sel_text ~= nil then
			local new_text = sel_text:lower();
			if new_text ~= sel_text then
				doc.replace(new_text, true);
			end ;
		end ;
		return 1;
	end;
	[0x01005] = function(self, id)
		local doc = mgr.current_document;
		if doc.content_type ~= content_type_cxx then
			-- 不是C++类型
			return 0;
		end ;
		return 1;
	end;
	-- 查找引用
	[_op.find_reference] = function(self, id)
		local doc = mgr.current_document;
		if lsp:find_reference(doc) then
			return 1;
		end;
		return 1;
	end;
	[_op.dump_bin] = function(self, id)
		local doc = mgr.current_document;
		local doc_name = doc.file_name:lower();
		local doc_ext_name = doc_name:match([[.+%.(%w+)]]);
		if doc.content_type == 0 or doc.doc_type == doc_type.hex then
			if  (
					doc_ext_name == 'dll' or
					doc_ext_name == 'ocx' or
					doc_ext_name == 'exe' or
					doc_ext_name == 'lib' or
					doc_ext_name == 'obj' or
					doc_ext_name == 'a' or
					doc_ext_name == 'o'
				)
			then
				local cmd = ([[%s\edx-tools\dumpbin.exe /NOLOGO /SUMMARY /HEADERS /SYMBOLS /EXPORTS /IMPORTS /ARCHIVEMEMBERS /DEPENDENTS "%s"]]):format(edx.app_path, doc_name);
				edx.shell(cmd, "new:text");
			elseif doc_ext_name == 'apk' then
				local cmd = ([[pushd %s\edx-tools && aapt.exe d badging "%s" && echo -------- && echo File Content: && aapt l -a "%s"]]):format(edx.app_path, doc_name, doc_name);
				edx.shell(cmd, "new:text");
			end ;
			return 0;
		end ;
		return 1;
	end;
	-- 重复选择内容
	[0x01007] = function(self, id)
		local doc = mgr.current_document;
		local sel_text = doc.selection_text;
		local fl, fp, el, ep = doc.get_selection();
		if sel_text ~= nil then
			doc.set_cursor(el, ep);
			doc.set_text(sel_text);
			doc.select_text(el, ep, doc.cursor_line, doc.cursor_column);
			doc.reset_render();
		else
			fl = doc.cursor_line;
			fp = doc.cursor_column;
			doc.select_text(fl, 0, fl, -1);
			sel_text = doc.selection_text;
			doc.set_cursor(fl, 0);
			doc.set_text(sel_text .. "\n");
			doc.set_cursor(fl + 1, fp);
		end ;
	end;
	-- 查找所有实现
	[_op.find_implementation] = function(self, id)
		local doc = mgr.current_document;
		lsp:find_implementation(doc);
		return 0;
	end;
	-- 加载当前文件为系统脚本/C++列出文件结构
	[0x01009] = function(self, id)
		local doc = mgr.current_document;
		if doc.content_type == content_type_cxx then
			-- c/c++ file
			lsp:list_doc_structure(doc, mgr.popup_class_view);
			return;
		end;
		local file_name = doc.file_name;
		local run_func = function()
			dofile(file_name);
		end;
		if file_name == nil or #file_name == 0 then
			local line_index = 0;
			local internal_name = doc.internal_name or "<NONAME>"
			local func, err_info = load(
				function()
					local line;
					line = doc:get_text(line_index, 0, 4096);
					line_index = line_index + 1;
					if line ~= nil then
						line = line.."\r\n";
					end;
					return line;
				end, internal_name
			);
			if func == nil then
				print("can not load lua script! ");
				err_info = err_info:gsub("([ \t]*)%[string \"(doc://<%x*>)\"]:(%d+):","%1%2:%3:");
				print(err_info);
				edx:active("output");
				return;
			else
				doc.content_type = content_type_lua;
			end
			run_func = func;
		end
		xpcall(run_func,
			function(msg)
				self.clear("output");
				print("error:\r\n");
				msg = debug.traceback(msg);
				msg = msg:gsub("([ \t]*)%[string \"(doc://<%x*>)\"]:(%d+):","%1%2:%3:");
				msg = msg:gsub("\n", "\r\n");
				print(msg);
				print("\r\n");
				edx:active("output");
				return 0;
			end
		);
	end;
	-- hover info
	[0x0100A] = function(self, id)
		local doc = mgr.current_document;
		lsp:hover_info(doc);
		return 1;
	end;
	[_op.jump_to_definition] = function(self, id)
		local doc = mgr.current_document;
		lsp:find_definition(doc);
		return 1;
	end;
	[_op.jump_to_declaration] = function(self, id)
		local doc = mgr.current_document;
		lsp:find_declaration(doc);
		return 1;
	end;
	[_op.close_other_files] = function(self, id)
		local cur_doc = mgr.current_document;
		local all_docs = {mgr.get_all_documents()};
		for i, doc in ipairs(all_docs) do
			if cur_doc ~= doc then
				mgr.close_doc(doc);
			end;
		end;
		cur_doc:redraw();
	end;
	[_op.open_file_dir] = function(self, id)
		local cur_doc = mgr.current_document;
		edx.shell_execute("open", "explorer.exe", [[/select,"]]..cur_doc.file_name..[["]]);
	end;
	[_op.open_file_terminal] = function(self, id)
		local cur_doc = mgr.current_document;
		local open_path = make_path(cur_doc.file_name, "..");
		open_terminal(open_path);
	end;
	-- change format
	[CMAKE_FMT_ID_BASE] = function (self, id)
		id = id - CMAKE_FMT_ID_BASE;
		local is_reload = (id < 0x10);
		local format = id % 0x10;
		local doc = mgr.current_document;
		doc.change_format(is_reload, format);
	end;
	[SYNTAX_MENU_ID_BASE] = function(self, id)
		id = id - SYNTAX_MENU_ID_BASE;
		local doc = mgr.current_document;
		doc.content_type = id;
		return 1;
	end;
	[LINE_FORMAT_MENU_ID_BASE] = function(self, id)
		id = id - LINE_FORMAT_MENU_ID_BASE;
		local doc = mgr.current_document;
		doc.line_format = id;
		return 1;
	end;
	[_op.open_tab_dir] = function (self, id)
		if edx.__last_tab_doc then
			edx.shell_execute("open", "explorer.exe", [[/select,"]]..edx.__last_tab_doc..[["]]);
			edx.__last_tab_doc = nil;
		end;
	end;
	[_op.open_tab_terminal] = function (self, id)
		if edx.__last_tab_doc then
			local open_path = make_path(edx.__last_tab_doc, "..");
			open_terminal(open_path);
			edx.__last_tab_doc = nil;
		end;
	end;
	[_op.copy_tab_path] = function (self, id)
		if edx.__last_tab_doc then
			mgr.set_clipboard(edx.__last_tab_doc);
			edx.__last_tab_doc = nil;
		end;
	end;
	[_op.close_tab] = function (self, id)
		if edx.__last_tab_doc then
			mgr.close_doc(edx.__last_tab_doc);
			mgr.current_document.redraw();
			edx.__last_tab_doc = nil;
		end;
	end;
	[_op.locate_tab_file] = function (self, id)
		if edx.__last_tab_doc then
			xws_mgr:select_item(edx.__last_tab_doc);
			edx:active("solution");
			edx.__last_tab_doc = nil;
		end;
	end;
};

function mgr:on_cmd(id, flags)
    if sizeof(self.__last_cmd) > 10 then
        table.remove(self.__last_cmd);
    end;
    table.insert(self.__last_cmd, 1, id);
	local op_id = id;
    local item;
    if id >= 0xE000 and id <= 0xFFFF then
        item = id - (id % 0x20);
    else
        item = id;
    end;
	item = text_cmd_map[item];
	-- fallback to previous command handler
	while id >= 0xE000 and item == nil do
		id = id - 0x20;
		item = text_cmd_map[id];
	end;
    if item ~= nil then
		return item(self, op_id, flags);
    end ;
    return 0;
end;

function edx:on_locate_output(info)
    -- match for vc
    out = { info:match("^%s*([a-zA-Z]*:?[^<>?*:;'\"]+)%((%d+)%)%s*:") };
    if out[1] == nil or out[2] == nil then
        out = { info:match("^%s*(doc://<[%d%w]+>):(%d+),(%d+):(%d+),(%d+):") };
    else
        -- vc output has no column info
        out[3] = 1;
    end;
    if out[1] == nil or out[2] == nil then
        out = { info:match("^%s*(doc://<[%d%w]+>):(%d+):") };
		out[3] = 1;
	end;
	if out[1] == nil or out[2] == nil then
		-- clang tips
        out = { info:match("^%s*In file included from ([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+)[:,]") };
        out[3] = 1;
    end;
	if out[1] == nil or out[2] == nil then
		-- gcc tips
		out = { info:match("^%s*inlined from.*%s+at%s+([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+):(%d+)[:,]") };
    end;
    if out[1] == nil or out[2] == nil then
        out = { info:match("^%s*from ([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+)[:,]") };
        out[3] = 1;
    end;
	if out[1] == nil or out[2] == nil then
		-- clang
        out = { info:match("^%s*([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+):(%d+):") };
    end;
	if out[1] == nil or out[2] == nil then
		-- clang-cl
		out = { info:match("^%s*([a-zA-Z]?:?[^<>?*:;'\"]+)%((%d+),(%d+)%):") };
    end;
    if out[1] == nil or out[2] == nil then
        out = { info:match("^%s*([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+):") };
        out[3] = 1;
    end;
    if out[1] == nil or out[2] == nil then
        out = { info:match("^([a-zA-Z]?:?[^<>?*:;'\"]+):%d+%((%d+)%)") };
        out[3] = 1;
    end;
    if out[1] == nil or out[2] == nil then
        out = { info:match("^%s*([a-zA-Z]?:?[^<>?*:;'\"]+):(%d+),(%d+):(%d+),(%d+):") };
    end;
    if out[1] == nil or out[2] == nil then
		out = { info:match("^%s*(\\\\wsl%$\\[^<>?*:;'\"]+):(%d+),(%d+):(%d+),(%d+):") };
    end;
    if out[1] == nil or out[2] == nil then
        -- for keil C51 compiler
        out = { info:match("^%s*[*]+.*IN LINE (%d+) OF ([a-zA-Z]?:?[^<>?*:;'\"]+):") };
        out[1], out[2], out[3] = out[2], out[1], 1
        -- print("KEIL: "..info..(([[ -- 1:%s, 2:%s, 3:%s]]):format(out[1],out[2],out[3])));
    end;
    if out[1] == nil or out[2] == nil then
        return 0;
    end ;
	local file_name = out[1];
	if file_name:match([[^\\wsl%$\.*$]]) then
		-- keep the wsl path
	elseif __last_toolset and __last_toolset.unmap_path then
		local unmap_path = __last_toolset.unmap_path;
		file_name = unmap_path(file_name);
	end;
	
    if CMAKE_PROJECT_PATH ~= nil and utils:file_time(file_name) == nil and file_name:sub(0,7) ~= "doc://<" then
        file_name = make_path(CMAKE_PROJECT_PATH, "dummy-build-path", file_name);
	end;

    if mgr.open_doc(file_name) then
        mgr.record_cursor_history();
        local cur_doc = mgr.current_document;
        cur_doc:set_cursor(tonumber(out[2]) - 1, tonumber(out[3]) - 1);
        if out[4] ~= nil and out[5] ~= nil then
            cur_doc:select_text(tonumber(out[2]) - 1, tonumber(out[3]) - 1, tonumber(out[4]) - 1, tonumber(out[5]) - 1);
        end ;
        edx.do_cmd(_op.center_cursor_line);
    end;
end;

function edx:store_breakpoints(doc)
	if CMAKE_PROJECT_PATH == nil then
		return;
	end;
	if project_configs == nil then
		project_configs = empty_project_configs();
	end;

	local name = doc.file_name;
	if name:sub(0,	#CMAKE_PROJECT_PATH):lower() ~= CMAKE_PROJECT_PATH:lower() then
		return;
	end;
	local short_name = name:sub(2+#CMAKE_PROJECT_PATH);
	short_name = short_name:gsub('\\', '/');
	local bkp_lines = {doc.get_all_breakpoints()};
	local breakpoints = project_configs[".breakpoints"];
	if breakpoints == nil then
		breakpoints = {};
		project_configs[".breakpoints"] = breakpoints;
	end;
	if #bkp_lines <= 0 then
		breakpoints[short_name] = nil;
		return;
	end;
	local bkp_info = {};
	breakpoints[short_name] = bkp_info;
	for i, ln in ipairs(bkp_lines) do
		bkp_info[ln] = {};
	end;
end;

function edx:store_file_states(doc)
	if CMAKE_PROJECT_PATH == nil then
		return;
	end;
	if project_configs == nil then
		project_configs = empty_project_configs();
	end;

	local name = doc.file_name;
	if name:sub(0,	#CMAKE_PROJECT_PATH):lower() ~= CMAKE_PROJECT_PATH:lower() then
		return;
	end;
	local short_name = name:sub(2+#CMAKE_PROJECT_PATH);
	short_name = short_name:gsub('\\', '/');
	local editor = project_configs[".editor"];
	if editor == nil then
		editor = {opened={};states={}};
		project_configs[".editor"] = editor;
	end;
	editor.states[short_name] = {
		cursor = {doc.cursor_line;doc.cursor_column;};
		fold = doc.fold_status;
	};
end;

function edx:restore_breakpoints(doc)
	if CMAKE_PROJECT_PATH == nil or project_configs == nil then
		return;
	end;

	local name = doc.file_name;
	if name:sub(0,	#CMAKE_PROJECT_PATH):lower() ~= CMAKE_PROJECT_PATH:lower() then
		return;
	end;
	local short_name = name:sub(2+#CMAKE_PROJECT_PATH);
	short_name = short_name:gsub('\\', '/');
	local breakpoints = project_configs[".breakpoints"] or {};
	local bkp_infos = breakpoints[short_name];
	if bkp_infos == nil then
		return;
	end;
	for ln, info in pairs(bkp_infos) do
		doc:toggle_breakpoint(ln);
	end;
end;

function edx:restore_file_states(doc)
	if CMAKE_PROJECT_PATH == nil or project_configs == nil then
		return;
	end;

	local name = doc.file_name;
	if name:sub(0,	#CMAKE_PROJECT_PATH):lower() ~= CMAKE_PROJECT_PATH:lower() then
		return;
	end;
	local short_name = name:sub(2+#CMAKE_PROJECT_PATH);
	short_name = short_name:gsub('\\', '/');
	local editor = project_configs[".editor"] or {states={}};
	local states = editor.states[short_name] or {cursor={0;0}};
	local line = states.cursor[1];
	local column = states.cursor[2];
	doc.set_cursor(line, column);
	if states.fold then
		doc.fold_status = states.fold;
	end;
	doc.center_cursor_line();
end;

dbg_call(
        function()
            update_short_cuts(-1, { _op.build_project, k("ctrl+shift+b") });    -- 执行make
            update_short_cuts(-1, { _op.build_and_run, k("ctrl+f5") });
			update_short_cuts(-1, { _op.rebuild_project, k("ctrl+alt+f7") });
			update_short_cuts(-1, { _op.search_everywhere, k("ctrl+/") }); -- search everywhere

			update_short_cuts( _op_scope._global, { _op.debug_start, k("f5") } );
			update_short_cuts( _op_scope._global, { _op.debug_step_over, k("f10") } );
			update_short_cuts( _op_scope._global, { _op.debug_step_in, k("f11") } );
			update_short_cuts( _op_scope._global, { _op.debug_step_out, k("shift+f11") } );
			update_short_cuts( _op_scope._global, { _op.debug_terminate, k("shift+f5") } );
			update_short_cuts( _op_scope._global, { _op.debug_toggle_break_point, k("f9") } );
			update_short_cuts( _op_scope._global, { _op.debug_break, k("ctrl+f9") } );
			update_short_cuts( _op_scope._global, { _op.debug_jump_to, k("alt+f9") } );
			update_short_cuts( _op_scope._global, { _op.locate_current_document, k("alt+l") } );


			update_short_cuts(_op_scope.text, { 0x01003, k("ctrl+shift+u") });    -- toupper
            update_short_cuts(_op_scope.text, { 0x01004, k("ctrl+u") });    -- tolower
            update_short_cuts(_op_scope.text, { 0x01005, k("ctrl+k"), k("ctrl+i") });    -- show C/C++ context info
			update_short_cuts(_op_scope.text, { _op.find_reference, k("shift+f12") });	-- 查找所有引用
			update_short_cuts(_op_scope.hex, { _op.dump_bin, k("shift+f12") });	-- DUMP二进制文件信息
            update_short_cuts(_op_scope.text, { 0x01007, k("ctrl+d") }); -- duplicate selection
			update_short_cuts(_op_scope.text, { 0x01009, k("ctrl+f12") });    -- 加载当前文件为系统脚本/C++列出文件结构
			update_short_cuts(_op_scope.text, { 0x0100A, k("ctrl+q") });    -- hover info

            -- 加载EDX环境
            edx:load_env();
            -- 切换到默认的布局
            edx:switch_layout("simple");

			make_edx_completion();
        end,
		LANG("notify/error/init_edx_failed")
);

-- 加载debug绑定
require "dbg"

function list_toolsets()
	if TOOLSETS == nil or #TOOLSETS == 0 then
		print("no toolset was found!");
		return;
	end;
	print("found c/c++ compiler toolset!");
	for i, toolset in ipairs(TOOLSETS) do
		print("\t", i, ":", toolset.name, " => ", toolset.home);
	end ;
end;

function update_toolsets()
	search_compilers(function(toolsets)
		TOOLSETS = toolsets;
		if toolsets ~= nil then
			list_toolsets();
			edx:load_toolset();
			menu_bar.update_cmake_toolset(toolsets, nil, function(tool)
					return tool.name, tool.name.."\t"..tool.home;
			end);
		end ;
	end);
end;

dbg_call(update_toolsets, LANG("notify/error/init_toolset_failed"));

require "project_wizard"
require "configuration"

-- 加载启动脚本
if utils:file_time(".\\settings.lua") then
	dbg_call(function()
			dofile(".\\settings.lua");
		end,
		LANG("notify/error/load_setting_failed")
	)
end;
